Repository navigation
Description
This is an analysis I ran with Claude to investigate what we might be able to vendor so that we could it on in an offline installation. This is a follow on to the work in #55461 which added the initial vendor-some-dependencies-necessary to even run. With that in place, I wondered: could we support just enough of Parquet to make an Arrow package, built from source, without access to the internet more useful?
TL;DR, yes, though it means going from a ~5MB tarball to a ~14MB tarball (+ maintaining these trimming scripts, testing them, etc)
Describe the enhancement requested
When the R package builds libarrow from source without network access (e.g. on CRAN), nixlibs.R calls turn_off_all_optional_features(). That build has no Parquet, Dataset, JSON, compression codecs, RE2 or utf8proc. #55461 (GH-55454) added a way to ship thirdparty source tarballs inside the R package (tools/vendored, populated by tools/vendor_dependencies.sh during make sync-cpp) for xsimd and uriparser, which even a minimal build needs. The same mechanism could ship everything a full non-cloud build needs, including Parquet, and keep the package small.
Summary: trimmed tarballs for every non-cloud dependency (Parquet, Dataset, Acero, JSON, all codecs, RE2, utf8proc, mimalloc) add ~9.9 MB to the package (~14 MB total, compared with 4.2 MB today). Untrimmed they would add ~157 MB, almost all of it Boost (136 MB). I built libarrow from the trimmed tarballs with no downloads on macOS arm64 (details below).
Current size
- arrow 26.0.1 on CRAN: 4.2 MB
- + xsimd and uriparser vendored by GH-55454: [R][C++] Vendor URI parser for the R library #55461: 0.57 MB, so ~4.8 MB
Size by feature (.tar.gz, versions from cpp/thirdparty/versions.txt at the time of writing)
| Group | Dependency | Upstream | Trimmed |
|---|---|---|---|
| Already vendored (#55461) | xsimd 14.3.0 | 0.36 | 0.27 |
| uriparser 1.0.2 | 0.21 | 0.08 | |
Parquet (ARROW_PARQUET turns on thrift and simdjson; thrift turns on zlib and Boost) |
Boost 1.92.0 | 136.15 | 4.92 |
| thrift 0.25.0 | 4.83 | 0.94 | |
| simdjson 4.6.4 | 6.37 | 0.50 | |
| zlib 1.3.1 | 1.44 | 0.47 | |
| Snappy (in practice needed to read most Parquet files) | snappy 1.2.2 | 1.06 | 0.08 |
| Other codecs | zstd 1.5.7 | 2.32 | 0.88 |
| brotli 1.2.1 | 0.62 | 0.50 | |
| lz4 1.10.0 | 0.37 | 0.23 | |
| bzip2 1.0.8 | 0.77 | 0.09 | |
| Compute / JSON / allocator | re2 2023-03-01 | 0.37 | 0.30 |
| utf8proc 2.10.0 | 0.19 | 0.18 | |
| rapidjson | 1.06 | 0.15 | |
| mimalloc 3.4.1 | 1.35 | 0.35 | |
| Total | 157.47 | 9.94 |
Dataset and Acero need no further thirdparty dependencies. abseil isn't needed: the pinned re2 (2023-03-01) is older than re2's abseil dependency.
Resulting package size, including the 4.2 MB package itself:
| Option | Approx. package size |
|---|---|
| Today (minimal offline build) | 4.2 MB |
| Trimmed Parquet + Snappy (+ xsimd/uriparser) | ~11.5 MB |
| Trimmed full non-cloud set (smallest full-featured option) | ~14 MB |
| Upstream tarballs, only Boost trimmed | ~30 MB |
| Upstream tarballs, nothing trimmed | ~162 MB |
Boost is the only dependency that must be trimmed. Trimming the others saves ~16 MB.
Trimming steps
Boost (136 MB to 4.9 MB)
- Configure Boost standalone with the
BOOST_INCLUDE_LIBRARIESlist frombuild_boost()inThirdpartyToolchain.cmake. For a threaded build with Parquet, that'salgorithm;crc;numeric/conversion;scope_exit;throw_exception;tokenizer;locale;thread;uuid. Boost's CMake resolves the transitive dependencies, which come to 54 libraries. The libraries it adds appear as subdirectories of<build>/libs. - Keep only the top-level files,
tools/cmake, and those 54libs/*directories. - Delete top-level
doc/,status/,more/, and everything intools/excepttools/cmake. - Inside each kept library, delete
test,doc,example(s),bench(mark),perf,performance,metaandtoolsdirectories.
If multiprecision is added (only needed when the compiler has no native int128, see ARROW_BOOST_NEED_MULTIPRECISION), the trimmed tarball is 10.5 MB instead. gcc/clang/mingw all have native int128, so the smaller list should be enough for R. The library list depends on Arrow's CMake configuration, so it needs re-checking whenever Boost or build_boost() changes. The old cpp/build-support/trim-boost.sh (removed in #50885) did something similar for the b2-based Boost tarball.
Everything else
The generic step deletes test, tests, testdata, doc, docs, example(s), benchmark(s), bench and fuzz directories. On top of that:
| Dependency | Extra removals | Keep |
|---|---|---|
| thrift | tutorial/ test/ compiler/ contrib/ doc/ debian/, all of lib/ except lib/cpp and lib/CMakeLists.txt (Arrow builds with BUILD_COMPILER=OFF and WITH_*=OFF except C++) |
|
| simdjson | singleheader/ (20 MB uncompressed), images/ jsonexamples/ tools/ scripts/ |
|
| zlib | contrib/ |
test/, because zlib's CMake builds test/example.c unless ZLIB_BUILD_EXAMPLES=OFF |
| brotli | go/ python/ java/ csharp/ js/ research/ |
docs/, because install() copies the man pages |
| zstd | contrib/ zlibWrapper/, all of build/ except build/cmake |
|
| lz4 | all of build/ except build/cmake |
|
| bzip2 | manual.*, sample*, *.html |
|
| re2 | benchlog/ python/ app/ |
|
| rapidjson | bin/ thirdparty/ |
|
| mimalloc | ide/ |
bin/ (prebuilt Windows redirect DLLs; probably unneeded for static builds, but not checked) |
| snappy, utf8proc, xsimd, uriparser | generic step only |
Each trimmed tarball no longer matches the pinned ARROW_*_BUILD_SHA256_CHECKSUM in versions.txt, and the bundled build checks those hashes (URL_HASH). The R package has its own copy of the file, so sync-cpp would rewrite the checksums in r/tools/cpp/thirdparty/versions.txt to match the trimmed tarballs, the same way it already seds tools/cpp/CMakeLists.txt. (Alternatively, trim only Boost and ship the rest as-is: ~30 MB package.)
The scripts I used (directory names hard-code the current versions):
trim-boost.sh
#!/usr/bin/env bash
# Trim the Boost CMake source tarball to the libraries libarrow's bundled
# Boost build uses.
# Usage: trim-boost.sh boost-1.92.0-cmake.tar.gz OUT.tar.gz
set -euo pipefail
IN="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
OUT="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"
tar xzf "$IN"
SRC="$(ls -d boost-*)"
SRC_ABS="$WORK/$SRC"
# BOOST_INCLUDE_LIBRARIES from build_boost() in ThirdpartyToolchain.cmake for a
# threaded build with Parquet (thrift) and native int128. Add multiprecision if
# ARROW_BOOST_NEED_MULTIPRECISION would be true.
LIBS="algorithm;crc;numeric/conversion;scope_exit;throw_exception;tokenizer;locale;thread;uuid"
# Let Boost's CMake resolve transitive dependencies; each library it adds
# shows up as a directory under build/libs.
cmake -S "$SRC" -B build -DBOOST_INCLUDE_LIBRARIES="$LIBS" > /dev/null
KEEP=$(cd build/libs && find . -maxdepth 2 -type d ! -name CMakeFiles | sed 's|^\./||' |
while read -r d; do [ -f "$SRC_ABS/libs/$d/CMakeLists.txt" ] && echo "libs/$d" || true; done)
mkdir trimmed
(cd "$SRC" && tar cf - $(ls | grep -v '^libs$') $KEEP) | (mkdir "trimmed/$SRC" && tar xf - -C "trimmed/$SRC")
cd "trimmed/$SRC"
rm -rf doc status more
(cd tools && ls | grep -v '^cmake$' | xargs rm -rf)
find libs -depth -type d \( -name test -o -name doc -o -name example -o -name examples \
-o -name bench -o -name benchmark -o -name perf -o -name performance -o -name meta \
-o -name tools \) -prune -exec rm -rf {} +
cd ..
tar czf "$OUT" "$SRC"trim.sh (everything else)
#!/usr/bin/env bash
# Trim thirdparty source tarballs down to what libarrow's bundled build uses.
# Usage: trim.sh SRC_TARBALL_DIR OUT_DIR
set -euo pipefail
IN="$(cd "$1" && pwd)"
OUT="$(mkdir -p "$2" && cd "$2" && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
rmd() { for p in "$@"; do rm -rf "$p"; done; }
trim_generic() {
# dirs no bundled build of these deps compiles; pass names to keep as args
local keep=("$@")
find . -depth -type d \( -name test -o -name tests -o -name testdata \
-o -name doc -o -name docs -o -name example -o -name examples \
-o -name benchmark -o -name benchmarks -o -name bench -o -name fuzz \) \
-prune -print | while read -r d; do
for k in "${keep[@]+"${keep[@]}"}"; do [ "$d" = "./$k" ] && continue 2; done
rm -rf "$d"
done
}
pack() { # dir, output name
tar czf "$OUT/$2" "$1"
}
cd "$WORK"
for t in "$IN"/*; do tar xf "$t"; done
# thrift: only the C++ library is built (BUILD_COMPILER=OFF, WITH_*=OFF)
( cd thrift-0.25.0 && rmd tutorial test compiler contrib doc debian \
&& (cd lib && ls | grep -v -E '^(cpp|CMakeLists.txt)$' | xargs rm -rf) && trim_generic )
pack thrift-0.25.0 thrift-0.25.0.tar.gz
# simdjson: built from src/ + include/; singleheader/ is a 20 MB amalgamation
( cd simdjson-4.6.4 && rmd singleheader images jsonexamples tools scripts && trim_generic )
pack simdjson-4.6.4 simdjson-v4.6.4.tar.gz
# zlib builds test/example.c by default (ZLIB_BUILD_EXAMPLES isn't turned off)
( cd zlib-1.3.1 && rmd contrib && trim_generic test )
pack zlib-1.3.1 zlib-1.3.1.tar.gz
( cd snappy-1.2.2 && trim_generic )
pack snappy-1.2.2 snappy-1.2.2.tar.gz
( cd lz4-1.10.0 && trim_generic && (cd build && ls | grep -v '^cmake$' | xargs rm -rf) )
pack lz4-1.10.0 lz4-v1.10.0.tar.gz
( cd zstd-1.5.7 && rmd contrib zlibWrapper && trim_generic \
&& (cd build && ls | grep -v '^cmake$' | xargs rm -rf) )
pack zstd-1.5.7 zstd-1.5.7.tar.gz
# brotli installs man pages from docs/
( cd brotli-1.2.1 && rmd go python java csharp js research && trim_generic docs )
pack brotli-1.2.1 brotli-v1.2.1.tar.gz
( cd bzip2-1.0.8 && rm -f manual.* sample* *.html && trim_generic )
pack bzip2-1.0.8 bzip2-1.0.8.tar.gz
( cd re2-2023-03-01 && rmd benchlog python app && trim_generic )
pack re2-2023-03-01 re2-2023-03-01.tar.gz
( cd utf8proc-2.10.0 && trim_generic )
pack utf8proc-2.10.0 utf8proc-v2.10.0.tar.gz
R=rapidjson-232389d4f1012dddec4ef84861face2d2ba85709
( cd $R && rmd bin thirdparty && trim_generic )
pack $R $R.tar.gz
( cd mimalloc-3.4.1 && rmd ide && trim_generic )
pack mimalloc-3.4.1 mimalloc-v3.4.1.tar.gz
( cd xsimd-14.3.0 && trim_generic )
pack xsimd-14.3.0 xsimd-14.3.0.tar.gz
( cd uriparser-1.0.2 && trim_generic )
tar cjf "$OUT/uriparser-1.0.2.tar.bz2" uriparser-1.0.2Verification
On macOS arm64 (Apple clang), I ran r/inst/build_arrow_static.sh with LIBARROW_MINIMAL=false ARROW_DEPENDENCY_SOURCE=BUNDLED ARROW_S3=OFF ARROW_GCS=OFF ARROW_AZURE=OFF ARROW_JEMALLOC=OFF. Every ARROW_*_URL pointed at a trimmed local tarball, and the checksums in versions.txt were rewritten to match. The FetchContent/ExternalProject logs confirm that every dependency came from a local file. libarrow, libarrow_compute, libarrow_acero, libarrow_dataset, libparquet and libarrow_bundled_dependencies all built and installed.
Not yet tested: Linux, Windows (Rtools), and actually installing and checking the R package.
What would need to change
tools/vendor_dependencies.sh: vendor the dependencies above, apply the trimming, and rewrite the checksums inr/tools/cpp/thirdparty/versions.txt.nixlibs.R: offline with nothirdparty_dependency_dir, the build currently callsturn_off_all_optional_features()(around L630-644). It should instead turn off only the features whose tarballs aren't vendored. S3 and Azure need care, because they default to ON on macOS. The existingset_thirdparty_urls()/create_package_with_all_dependencies()code already handles "build from a directory of tarballs" and could probably be reused.- LICENSE/NOTICE: add entries for the newly shipped sources (Boost Software License, thrift, simdjson, the codecs, etc.).
Other considerations
- CRAN policy: CRAN asks packages to be "of the minimum necessary size", so a ~14 MB source tarball may need justifying.
- Build time: building thrift, Boost.Locale/Thread and Parquet adds noticeable compile time to CRAN source builds.
Cloud filesystems (numbers only, not proposed)
For reference, the cloud filesystems are much more expensive and also need system libraries that an offline build can't get:
| Feature | Tarballs (upstream) | Rough trimmed estimate | Also needs |
|---|---|---|---|
| S3 | aws-sdk-cpp 77.7 MB, aws-lc 138.5 MB (Linux), s2n 4.9 MB, aws-c-* ~5.3 MB (~226 MB) | ~25 MB (SDK limited to config core identity-management s3 sts transfer = 2.1 MB; aws-lc without test vectors and fuzz/ = 13 MB). Not build-tested |
system libcurl, OpenSSL |
| GCS | google-cloud-cpp 16.2 MB, nlohmann-json 9.2 MB, abseil 2.3 MB (~28 MB) | not measured | system libcurl, OpenSSL |
| Azure | azure-sdk-for-cpp 3.0 MB | not measured | system libcurl, libxml2, OpenSSL |
Component(s)
R
Activity
jonkeane commentedon Nov 11, 2026
I've opened two PRs as options, we would only merge one of #55491 or #55492 not both.