You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add buildroot.py, a self-contained incremental sync script for sources.buildroot.net.
Why
sources.buildroot.net (the official Buildroot source backup) has permanently disabled directory listing (Cloudflare 403 on every path that isn't a known file) and offers no rsync module:
Tool
Result
rsync
no rsync module offered
tsumugu / wget --mirror
cannot enumerate; every directory request 403
recursive HTML scraping
no HTML index exists
The Buildroot maintainers have stated on the mailing list that they will not enable rsync, so a mirror has to derive the file list from somewhere else. This script derives it from the buildroot git tree.
How it works
Shallow-clone the buildroot master branch
Parse boot/, linux/, package/, toolchain/, utils/.mk files for VERSION/SITE/SOURCE
Expand version variables (including $(subst)), Kconfig mirror defaults (BR2_GNU_MIRROR, BR2_KERNEL_MIRROR, BR2_LUAROCKS_MIRROR, BR2_CPAN_MIRROR), the github/gitlab/sourceforge macros, and pkg-download.mk's VCS filename suffixes (-git4/-svn5/-cargo6/-go2)
HEAD-compare each candidate URL against the local mirror by Content-Length before skipping
Download new/changed files atomically (.tmp + rename), trying the backup site (TUNASYNC_UPSTREAM_URL) first — it is canonical for this mirror — and the package's own site as fallback
Optionally clean up stale files (opt-in, capped)
Existing local data (478G at NJU) is preserved: the script only adds/updates files unless cleanup is explicitly enabled.
Environment variables
Variable
Default
Meaning
TUNASYNC_WORKING_DIR
(required)
mirror data directory
TUNASYNC_UPSTREAM_URL
http://sources.buildroot.net/
backup site, tried first
TUNASYNC_BUILDROOT_GIT
https://github.com/buildroot/buildroot.git
metadata source
TUNASYNC_BUILDROOT_BRANCH
master
branch to parse
TUNASYNC_BUILDROOT_JOBS
1
parallel downloads (serial by default)
TUNASYNC_BUILDROOT_CLEANUP
off
1 enables stale-file deletion
TUNASYNC_BUILDROOT_MAXDELETE
10000
safety cap for cleanup
TUNASYNC_BUILDROOT_DRYRUN
off
log only, write nothing
TUNASYNC_BUILDROOT_WGET_TIMEOUT
3600
per-file download cap (s)
https_proxy / http_proxy
honored for downloads
Safety properties
wget replaces the destination only on success; a failed download never deletes a known-good mirrored file
the state file advances only after a fully successful non-dry run; an unchanged HEAD still runs a full verification pass, so files deleted or corrupted since the last run self-heal
an extraction yielding 0 packages aborts before touching state or local files; cleanup refuses to run on an empty extraction and is skipped whenever any download failed
Kconfig-driven packages (linux/gcc/uboot via $(call qstrip,$(BR2_...))) are counted as unresolvable, not failures — full resolution would need make show-info and is out of scope; mirroring covers master only (per-branch/LTS tracking is a deliberate limitation, and cleanup stays opt-in because of it)
Testing
Smoke-tested the macro emit helpers against buildroot master (aichat/z3/leafnode2 archive refs, wf111 conditional sources, zip's $(subst)), archive-extension detection, and the zero-package cleanup fuse, plus a live extraction pass over the full tree. Running in production at mirror.nju.edu.cn against the existing 478G mirror.
Deployment
Stdlib Python 3 + wget + git, all already present in the standard tunathu/tunasync-scripts image — no image change needed.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a self-contained Python sync script to mirror Buildroot package sources without relying on upstream directory listing, by extracting download URLs from the Buildroot git tree and incrementally downloading missing artifacts.
Changes:
Introduces buildroot.py to clone/update Buildroot git, parse .mk metadata, and download package sources incrementally.
Adds basic logging + state tracking to skip runs when git HEAD hasn’t changed.
Implements atomic downloads via .tmp → rename with fallback to sources.buildroot.net.
Update from production (mirror.nju.edu.cn): packages whose .mk leaves a variable unexpanded (e.g. $(FOO_VERSION) in the computed filename) used to create garbage files on disk. 5b5cbb3 now detects $(/${ in the local filename, logs it, counts the package as failed, and skips the download. The .tmp cleanup part of that production fix was already covered here by the finally-block in wget().
Full tarball URLs re-appended with a filename — fixed: sync_package() detects URLs that already end in a filename (url_has_filename(), e.g. github/gitlab archive URLs) and uses them as-is; ...tar.gz/<file> URLs are no longer produced.
stats['total'] under-counting — fixed: total is bumped at the top of sync_package(), before any skip return, so progress logging and the summary cover every processed package.
Leftover .tmp on download failure — fixed: .tmp is removed via a finally block on every code path, and the wget stderr tail (and subprocess exceptions) are now logged on failure (511d86f).
**:=' assignments missed** — fixed: the assignment regex now accepts [:?+]?=`.
JOBS/MAXDELETE printed but ineffective — resolved by making the knobs real instead of removing them: downloads parallelize via ThreadPoolExecutor when TUNASYNC_BUILDROOT_JOBS>1 (default 1 = serial), and cleanup is functional but opt-in via TUNASYNC_BUILDROOT_CLEANUP, bounded by TUNASYNC_BUILDROOT_MAXDELETE.
git fetch/reset return codes ignored — fixed: rc is checked and stderr logged for fetch/reset/clone, and the run aborts rc=1 when no git tree is available instead of syncing an empty package list. The 180s timeout in run() is unchanged.
The reason will be displayed to describe this comment to others. Learn more.
I ran extract_packages() / sync_package() from this PR against a fresh shallow clone of buildroot master. Several issues are reproducible (details inline). The three most serious are: HTML pages being saved as tarballs, the GitHub tag being ignored, and wrong filenames for git/svn/cargo/go packages.
Design concerns:
Master only: LTS branches (e.g. 2025.02.x) are never mirrored, and with cleanup enabled, every older version still used by LTS users would be deleted.
Sources whose version comes from Kconfig are never mirrored: linux, uboot, gcc, arm-trusted-firmware, etc. use $(call qstrip,$(BR2_...)), so they are skipped as "unexpanded". Sites based on $(BR2_GNU_MIRROR) / $(BR2_KERNEL_MIRROR) are silently dropped, and variables defined in another file (e.g. QT6BASE_VERSION = $(QT6_VERSION)) are not resolved, so qt6base is missing entirely. As a side effect the script almost always exits with 2.
Parsing Make by regex is fragile (+= overwrites instead of appending, and inside ifeq blocks the last assignment wins). Consider letting Buildroot produce the list itself (make <defconfig> show-info / source gives the exact filenames and URIs), e.g. across all defconfigs. It is slower, but correct.
Copilot's second round is still unaddressed in the code: cleanup runs when the extracted package list is empty, there is no sourceforge macro support, there is no Content-Length check (both are promised in the description), and UPSTREAM is unused.
Minor: .buildroot-sync.log lives in the published directory and grows without bound; import threading sits in the middle of the file; TimeoutExpired from run() is not caught.
The reason will be displayed to describe this comment to others. Learn more.
Saves HTML pages as tarballs. Any last path segment containing a . is treated as a filename. That matches versioned directories (https://python.org/ftp/python/3.14.7, .../releases/download/16.1) and .git repository URLs. On current master, 515 of 2903 candidates are affected. wget then downloads the directory listing or the repo page and stores it under the tarball name:
wd/python3/Python-3.14.7.tar.xz: HTML document, ASCII text
wd/aer-inject/aer-inject-9bd5e2c7...tar.gz: HTML document, Unicode text
Because of the existence-only check at L306, these files are never replaced, and Buildroot users who fall back to the mirror get a hash mismatch. It is worth checking the existing mirror with file(1). The distinction should come from the source (macro vs. plain SITE), not from a heuristic on the URL. Verifying against the package's .hash file (sha256_file is already there, just unused) would also catch this.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: filename detection uses an archive-extension whitelist (since round 2), and with the backup site now tried first, the Content-Length comparison automatically re-downloads HTML error pages previously saved under a tarball name. .hash-based verification is left as a follow-up.
The reason will be displayed to describe this comment to others. Learn more.
The tag from $(call github,...) is ignored. The regex captures version, but emit_github is passed the bare *_VERSION. Any prefix is lost: AICHAT_SITE = $(call github,sigoden,aichat,v$(AICHAT_VERSION)) becomes https://github.com/sigoden/aichat/archive/0.30.0/aichat-0.30.0.tar.gz, which returns 404. About 377 packages build the tag like this. Use m.group("version") (the same applies to gitlab).
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: emit_github now receives the third macro argument verbatim, so prefixed refs resolve correctly -- verified against buildroot master: aichat -> archive/v0.30.0, z3 -> archive/z3-4.16.0.
The reason will be displayed to describe this comment to others. Learn more.
Wrong default filename for git/svn/cargo/go packages. Buildroot uses $(RAWNAME)-$(VERSION)$(pkg_source_ext), and pkg_source_ext adds -git4, -svn5, -cargo6 or -go2 depending on SITE_METHOD / DOWNLOAD_POST_PROCESS (see package/pkg-utils.mk). Example:
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: the default SOURCE now appends pkg-download.mk's suffixes (-git4/-svn5 from SITE_METHOD, -go2/-cargo6 from the download post-process, including $(eval $(cargo-package))/$(golang-package)). 159 packages get the suffixed name, and VCS-method packages whose SITE is a repo URL now fall back to the backup site only.
The reason will be displayed to describe this comment to others. Learn more.
Stripping $(...) here silently drops every package whose SITE starts with $(BR2_GNU_MIRROR) / $(BR2_KERNEL_MIRROR). The result doesn't start with http, so the package does not even reach the fallback to sources.buildroot.net. If a variable in the middle of the URL is stripped, the result is a nonsense URL.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: BR2_GNU_MIRROR, BR2_KERNEL_MIRROR, BR2_LUAROCKS_MIRROR and BR2_CPAN_MIRROR are seeded from their Config.in defaults, and a two-pass scan resolves cross-file references -- 93 gnu/kernel-mirror packages recovered (verified on master).
The reason will be displayed to describe this comment to others. Learn more.
sources.buildroot.net is only the fallback, after the original upstream. For a mirror of the backup site the order should be reversed: the backup copy is the canonical file (e.g. for non-reproducible GitHub tarballs), and reversing it also reduces load on upstream projects. UPSTREAM (L46) should be used here instead of the hard-coded _BR_PRIMARY_SITE.
The reason will be displayed to describe this comment to others. Learn more.
The state is written even after failed downloads and in DRYRUN mode. A dry run therefore permanently suppresses the next real sync, and failed files are not retried until master moves. Write it only after a real run with no failures. Relatedly, L422 returns 0 when there are no new commits even though the previous run ended with 2, so the tunasync status flips back and forth.
The reason will be displayed to describe this comment to others. Learn more.
State is only written after a non-dry run with zero failures (since round 2); in 1e11a0a an unchanged HEAD also no longer returns early -- it runs a full verification pass so an interrupted run self-heals.
The reason will be displayed to describe this comment to others. Learn more.
A 300 s hard timeout kills large downloads (linux, gcc, qt) on slower links, and they then fail again on every run. Rely on wget's --timeout / --read-timeout, or make this configurable.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: the hard timeout is now configurable via TUNASYNC_BUILDROOT_WGET_TIMEOUT (default 3600 s); wget's own --timeout=60 network timeout is unchanged.
The reason will be displayed to describe this comment to others. Learn more.
Cleanup compares against master only, so with CLEANUP=1 it would delete every older version that users of LTS branches still download. It is currently prevented only because the real file count exceeds MAXDELETE=10000. It also still runs when packages is empty (Copilot's comment).
The reason will be displayed to describe this comment to others. Learn more.
The empty-extraction fuse is in place (cleanup refuses to run on 0 packages). On LTS: cleanup stays opt-in (off by default) and capped by TUNASYNC_BUILDROOT_MAXDELETE, but mirroring master only is a deliberate limitation -- per-branch tracking would need per-branch state and is out of scope for this PR.
Cleanup fuse: clean_stale_files() refuses to run when extraction yielded 0 packages, so a parser/tree failure can no longer make every local file look stale.
GitHub archive URL: emit_github()/emit_gitlab() now return the bare archive base URL exactly like Buildroot's github/gitlab helpers (https://github.com/<org>/<repo>/archive/<ref>), and sync_package() appends the actual source filename — so packages overriding <pkg>_SOURCE get the correct URL. (For the default source name the resulting URL is unchanged, matching what Buildroot itself downloads.)
SourceForge macro: explicit $(call sourceforge,<project>,<file>[,<version>]) expansion to downloads.sourceforge.net was added before the generic URL handling; such packages are no longer silently omitted.
Content-Length check: an existing non-empty file is no longer skipped blindly — the remote is HEADed and the file is re-downloaded on size mismatch (unknown remote size keeps the local copy, so HEAD rejection never causes re-download storms).
UPSTREAM honored: the fallback download path now uses TUNASYNC_UPSTREAM_URL (default http://sources.buildroot.net/) instead of the hard-coded site.
State file: only written after a non-dry run with zero failures; a transient failure no longer records the commit as complete, and dry runs never suppress the first real sync.
Smoke-tested: emit helpers, archive-extension detection, and the 0-package cleanup fuse.
VERSION_OVERRIDES is declared as the manual escape hatch described in the module documentation, but it is never read while selecting version. Adding an entry to this dict currently has no effect, so packages requiring an override remain unhandled; apply the dict during version selection.
The reason will be displayed to describe this comment to others. Learn more.
In 1e11a0a these Kconfig-driven packages (linux/gcc/uboot...) are counted as 'unresolvable' instead of failed, so they no longer fail the run. Fully resolving them needs make show-info / Kconfig evaluation, out of scope for this parser.
The reason will be displayed to describe this comment to others. Learn more.
This one is a false positive: per package/pkg-download.mk the github helper is the base https://github.com/$(1)/$(2)/archive/$(3) and Buildroot downloads /. sync_package appends the source filename to directory-style sites in exactly that way (verified: .../archive/v0.30.0/aichat-0.30.0-cargo6.tar.gz downloads fine).
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: conditional *_SOURCE reassignments (ifeq arch blocks) are collected as extra entries, so all three wf111 variants are mirrored (verified on master).
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged as a limitation: inner-package generated definitions need Make macro expansion that a regex parser cannot do; they are skipped rather than mis-parsed.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: wget only replaces dest on success and the failure path no longer unlinks the existing file, so the previously mirrored copy stays in place.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1e11a0a: an unchanged HEAD now runs a full verification pass instead of returning early, so files deleted or corrupted since the last run are reconciled.
sources.buildroot.net (the official Buildroot source backup site) has
permanently disabled directory listing (Cloudflare 403) and offers no
rsync module, so neither tsumugu nor wget -m can discover which files
exist. This script derives the file list from the buildroot git tree
instead: it parses boot/linux/package/toolchain .mk files for
VERSION/SITE/SOURCE, expands version variables (including $(subst)),
Kconfig mirror defaults (BR2_GNU_MIRROR & friends) and the
github/gitlab/sourceforge macros, applies pkg-download.mk's VCS
filename suffixes (-git4/-svn5/-cargo6/-go2), and downloads only new
or changed files.
Key design decisions (several shaped by review):
- The backup site (TUNASYNC_UPSTREAM_URL) is canonical for this
mirror and is tried FIRST; the package's own site is only a
fallback, which also reduces load on upstream projects.
- Existing files are HEAD-checked (Content-Length) before being
skipped, so republished/corrected same-name files are
re-downloaded; an unknown remote size always keeps the local copy.
- Downloads are atomic (.tmp + rename); wget replaces the
destination only on success, so a failed run never deletes
known-good mirrored data.
- A state file records the last fully synced commit, but an
unchanged HEAD still runs a full verification pass, so files
deleted or corrupted since the last run self-heal. State advances
only after a non-dry run with zero failures; an extraction that
yields zero packages aborts without touching state or local files.
- Stale-file cleanup is opt-in (TUNASYNC_BUILDROOT_CLEANUP), capped
by TUNASYNC_BUILDROOT_MAXDELETE, refuses to run on an empty
extraction, and is skipped entirely when any download failed.
- Packages whose version comes from Kconfig (linux/gcc/uboot via
$(call qstrip,$(BR2_...))) cannot be resolved by regex parsing;
they are counted as "unresolvable", not failures, so the run stays
green. Full resolution would need `make show-info` and is out of
scope. Mirroring covers master only; per-branch (LTS) tracking is
a deliberate limitation, and cleanup stays opt-in because of it.
- JOBS defaults to 1 (serial); the per-file download cap is
TUNASYNC_BUILDROOT_WGET_TIMEOUT (default 3600 s).
Smoke-tested: macro emit helpers (aichat/z3/leafnode2 archive refs,
wf111 conditional sources, zip's $(subst)), archive-extension
detection, and the zero-package cleanup fuse, plus a live extraction
pass against buildroot master. Existing local data (478G) is
preserved; running in production at mirror.nju.edu.cn.
Branch cleanup note: this branch has been squashed to a single commit, 08ecd52. All commit SHAs referenced earlier in the review threads (up to 1e11a0a) are now orphaned commits — the links still open, but please rely on the current diff, whose tree is byte-identical to the previous head 1e11a0a.
Review items → how they were addressed (all included in the current diff)
Maintainer review (reproduced against buildroot master):
HTML pages saved as tarballs (515/2903 candidates) → filename detection uses an archive-extension whitelist; with the backup site tried first, the Content-Length check re-downloads HTML error pages previously saved under tarball names. .hash-based verification is left as a follow-up.
Tag in $(call github,org,repo,TAG) ignored → the third macro argument is now passed verbatim (verified: aichat → archive/v0.30.0, z3 → archive/z3-4.16.0); same fix for gitlab.
Wrong default filename for git/svn/cargo/go packages → pkg-download.mk suffixes (-git4/-svn5/-cargo6/-go2) are applied; 159 packages get the suffixed name, and VCS-method packages fall back to the backup site.
$(BR2_GNU_MIRROR)/$(BR2_KERNEL_MIRROR) sites silently dropped → these (plus luarocks/CPAN) are seeded from their Config.in defaults with a two-pass scan; 93 gnu/kernel-mirror packages recovered.
Backup site should be tried first, UPSTREAM unused → TUNASYNC_UPSTREAM_URL is tried first; the package's own site is the fallback.
State written after failed/dry runs, status flapping → state advances only after a non-dry run with zero failures, and an unchanged HEAD now runs a full verification pass instead of exiting early.
300 s hard timeout kills large downloads → configurable TUNASYNC_BUILDROOT_WGET_TIMEOUT (default 3600 s); wget's own network timeout unchanged.
Cleanup vs. LTS branches → cleanup stays opt-in (off by default) and MAXDELETE-capped; mirroring master only is a documented, deliberate limitation.
"Let Buildroot produce the list (make show-info)" → acknowledged as the correct-but-heavy approach; out of scope for this regex parser, documented in the docstring.
Round 2: cleanup fuse on empty extraction, GitHub archive URL shape, sourceforge macro support, Content-Length check before skipping, UPSTREAM honored, state-file timing → all fixed.
Round 3: $(subst) support, Kconfig-driven packages counted as unresolvable instead of failed, conditional *_SOURCE reassignments collected (wf111), destination preserved on failed re-download, zero-extraction abort, crashed workers counted as failures, cleanup skipped on incomplete runs → all fixed; inner-*-package generated definitions are an acknowledged parser limitation (skipped, not mis-parsed). One round-3 finding (GitHub helper URL shape) was a false positive — per package/pkg-download.mk the helper is the base .../archive/<ref> and Buildroot downloads <site>/<source>, which is exactly how sync_package() uses it.
Some earlier in-thread replies described intermediate states of the branch; please rely on the current diff and this summary.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add
buildroot.py, a self-contained incremental sync script forsources.buildroot.net.Why
sources.buildroot.net(the official Buildroot source backup) has permanently disabled directory listing (Cloudflare 403 on every path that isn't a known file) and offers no rsync module:The Buildroot maintainers have stated on the mailing list that they will not enable rsync, so a mirror has to derive the file list from somewhere else. This script derives it from the buildroot git tree.
How it works
masterbranchboot/,linux/,package/,toolchain/,utils/.mkfiles forVERSION/SITE/SOURCE$(subst)), Kconfig mirror defaults (BR2_GNU_MIRROR,BR2_KERNEL_MIRROR,BR2_LUAROCKS_MIRROR,BR2_CPAN_MIRROR), thegithub/gitlab/sourceforgemacros, and pkg-download.mk's VCS filename suffixes (-git4/-svn5/-cargo6/-go2)Content-Lengthbefore skipping.tmp+ rename), trying the backup site (TUNASYNC_UPSTREAM_URL) first — it is canonical for this mirror — and the package's own site as fallbackExisting local data (478G at NJU) is preserved: the script only adds/updates files unless cleanup is explicitly enabled.
Environment variables
TUNASYNC_WORKING_DIRTUNASYNC_UPSTREAM_URLhttp://sources.buildroot.net/TUNASYNC_BUILDROOT_GIThttps://github.com/buildroot/buildroot.gitTUNASYNC_BUILDROOT_BRANCHmasterTUNASYNC_BUILDROOT_JOBS1TUNASYNC_BUILDROOT_CLEANUP1enables stale-file deletionTUNASYNC_BUILDROOT_MAXDELETE10000TUNASYNC_BUILDROOT_DRYRUNTUNASYNC_BUILDROOT_WGET_TIMEOUT3600https_proxy/http_proxySafety properties
$(call qstrip,$(BR2_...))) are counted asunresolvable, not failures — full resolution would needmake show-infoand is out of scope; mirroring coversmasteronly (per-branch/LTS tracking is a deliberate limitation, and cleanup stays opt-in because of it)Testing
Smoke-tested the macro emit helpers against buildroot master (
aichat/z3/leafnode2archive refs,wf111conditional sources,zip's$(subst)), archive-extension detection, and the zero-package cleanup fuse, plus a live extraction pass over the full tree. Running in production at mirror.nju.edu.cn against the existing 478G mirror.Deployment
Stdlib Python 3 +
wget+git, all already present in the standardtunathu/tunasync-scriptsimage — no image change needed.