redis/scripts/tarball.sh
Tal Bar Yakar 4dd58caa7c
Some checks are pending
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
RED-191626 , RED-191854 - modules modernization (#15253)
## Summary

Redis core and the external modules (Search, JSON, Bloom, TimeSeries)
are currently built and released through separate flows, forcing anyone
assembling a full distribution to juggle multiple repos, refs, build
commands, and config files.

This PR collapses that into one Redis-owned workflow:

```sh
make modules-update    # clone/refresh modules from modules/modules.yaml
make bootstrap         # install per-module build/test deps
make build             # build Redis core + selected modules
make run               # start redis-server with modules loaded
make test all          # run module test suites
make tarball TAG=...   # build a reproducible source bundle
```

## What This Adds

**1. Manifest-driven modules** — `modules/modules.yaml` is the single
source of truth for bundled modules. Each entry pins `repo`/`ref` and
defines its build artifact and runtime `loadmodule` path. Refs resolve
dynamically (tag → branch → SHA, else fail).

Current pins: RedisBloom `v8.9.81`, RediSearch `v8.9.82`, RedisJSON
`v8.9.81`, RedisTimeSeries `v8.9.82`.

**2. Central manifest parser** — `scripts/lib/manifest.sh` is the only
YAML reader, usable both as a shell library and a CLI helper (via
`modules/manifest.mk`).

**3. Per-module stub Makefiles removed** — The `modules/redis*/Makefile`
stubs are deleted; their data moves to `modules.yaml` and shared
behavior to `modules/common.mk`.

## Make Workflow

The root `Makefile` is now a thin dispatch layer over scripts in
`scripts/`:

| Target | Script | Purpose |
|---|---|---|
| `make build [args]` | `scripts/build.sh` | Build Redis core plus
selected modules |
| `make bootstrap [args]` | `scripts/bootstrap.sh` | Install per-module
build/test prerequisites |
| `make setup [args]` | `scripts/setup.sh` | Run module update plus
bootstrap |
| `make run [args]` | `scripts/run.sh` | Start Redis with selected
modules loaded |
| `make test [args]` | `scripts/test.sh` | Run Redis or module tests |
| `make clean [args]` | `scripts/clean.sh` | Clean Redis and/or module
outputs |
| `make deploy [args]` | `scripts/deploy.sh` | Install Redis and module
artifacts |
| `make modules-update [args]` | `scripts/modules-update.sh` |
Clone/refresh module sources from the manifest |
| `make modules-shallow [args]` | `scripts/modules-shallow.sh` |
Re-clone selected modules with shallow history |
| `make sync-redis-conf [args]` | `scripts/sync-redis-conf.sh` |
Regenerate managed module config |
| `make tarball TAG=...` | `scripts/tarball.sh` | Create a reproducible
Redis + modules source tarball |

All accept module selectors:

```sh
make build all              # core + all modules
make build redisjson        # core + one module
make run none               # core only, no modules
make test redisearch TEST='some:test:name'
```

## Runtime Configuration

Removes the hand-maintained `redis-full.conf` (which could drift from
upstream). Config is now generated: `sync-redis-conf.sh` produces a
managed Modules block appended to verbatim `redis.conf`, with
`loadmodule` lines (active when built or `ASSUME_BUILT=1`) plus each
module's `module.conf`. The generated file is git-ignored.

## Docker & Release

- **`docker/Dockerfile.noble`** — Ubuntu 24.04 build env for core +
modules (amd64/arm64).
- **`scripts/tarball.sh`** — reproducible source bundle: archives core
at `TAG`, clones modules at pinned refs, strips `.git`/build artifacts,
generates config, and produces a deterministic gzip.
`utils/releasetools/01_create_tarball.sh` now delegates to it.
- **`post-release-automation.yml`** — builds the bundle on published
releases, uploads it as artifact + release asset, and reports SHA256.

## Docs

`modules/MODULES.md` (operator guide), `modules/README.md`, and
`README.md` cover the manifest schema, Make targets, tarball flow,
Docker env, and troubleshooting.

## Compatibility Notes

- `BUILD_WITH_MODULES=yes` is replaced by `make build
[redis|core|none|all|<module>...]`.
- `redis-full.conf` is now generated and git-ignored.
- Module clones live under `modules/<name>/src/` (git-ignored).
- `vector-sets` is omitted — it already lives in-tree.
- RediSearch builds with `LTO=1 REDISEARCH_GENERATE_HEADERS=0
INLINE_LSE_ATOMICS=0`.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2026-07-08 22:55:45 +08:00

222 lines
9.1 KiB
Bash
Executable file

#!/bin/sh
# tarball.sh — build a self-contained, reproducible Redis+modules tarball.
#
# Usage: scripts/tarball.sh
# Required env: TAG=<git-ref> redis core ref to archive (e.g. 8.0.0)
# Optional env:
# STAGING_DIR default: /tmp/redis-tarball-staging-<tag>
# OUT_PATH default: /tmp/redis-<tag>.tar.gz
# TAR GNU tar binary (auto-detect: gtar > tar)
# TARBALL_SKIP_MODULES_UPDATE=1 skip the pre-step modules-update
#
# Produces redis-<tag>/ containing:
# - redis core source tree (from `git archive <tag>`)
# - modules/<name>/src/ at the pinned ref from modules.yaml, .git/.github stripped
# Output tar is sorted, with mtimes set to the tag's commit timestamp, owner=0.
set -eu
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" || exit 1
. "$SCRIPT_DIR/lib/manifest.sh"
cd "$REPO_ROOT"
MAKE_BIN="${MAKE:-make}"
TAR="${TAR:-$(command -v gtar 2>/dev/null || command -v tar 2>/dev/null)}"
# Validate cheap-to-check inputs BEFORE the expensive modules-update clone, so
# a missing/invalid TAG or unusable tar exits in milliseconds instead of after
# a multi-minute network operation.
if [ -z "${TAG:-}" ]; then
echo "ERROR: TAG=<tag> is required"
echo " e.g. 'make tarball TAG=8.0.0'"
exit 1
fi
if ! git rev-parse --verify -q "$TAG^{commit}" >/dev/null 2>&1; then
echo "ERROR: '$TAG' is not a valid git ref in this repo"
exit 1
fi
if [ -z "$TAR" ]; then
echo "ERROR: no tar binary found on PATH"; exit 1
fi
if ! "$TAR" --version 2>&1 | grep -qi 'GNU tar'; then
echo "ERROR: GNU tar required (found: $($TAR --version 2>&1 | head -1))"
echo " On macOS: brew install gnu-tar, then retry with TAR=gtar"
exit 1
fi
if [ "${TARBALL_SKIP_MODULES_UPDATE:-}" != "1" ]; then
echo "==> [tarball] pre-step: modules-update all (shallow)"
MODULES_UPDATE_SHALLOW=1 "$SCRIPT_DIR/modules-update.sh" all
echo
fi
staging="${STAGING_DIR:-/tmp/redis-tarball-staging-$TAG}"
out="${OUT_PATH:-/tmp/redis-$TAG.tar.gz}"
prefix="redis-$TAG"
work="$staging/$prefix"
# Paranoia: refuse to rm -rf any path that isn't clearly a temp/staging dir.
case "$staging" in
/tmp/*|/var/tmp/*|"$HOME"/.cache/*) ;;
*) echo "ERROR: refusing to rm -rf STAGING_DIR='$staging' (must be under /tmp, /var/tmp, or \$HOME/.cache)" >&2; exit 1 ;;
esac
echo "==> Staging at $staging"
rm -rf "$staging"
mkdir -p "$work"
echo "==> git archive Redis core @ $TAG$work"
# Route through a temp file rather than `git archive | tar`: POSIX sh (dash)
# has no `pipefail`, so a piped `git archive` failure would be masked by tar
# exiting 0. The redirect makes a git failure trip `set -e`.
_src_tar="$(mktemp)"
git archive --format=tar "$TAG" > "$_src_tar"
"$TAR" -x -C "$work" -f "$_src_tar"
rm -f "$_src_tar"
echo
echo "==> Cloning bundled modules per modules.yaml"
for name in $(manifest_modules); do
repo="$(manifest_field "$name" repo)"
ref="$(manifest_ref "$name")"
kind="$(manifest_ref_kind "$name")"
dest="$work/modules/$name/src"
mkdir -p "$work/modules/$name"
if [ -z "$ref" ]; then
echo "ERROR: '$name' must set a 'ref' in modules.yaml" >&2
exit 1
fi
if [ -z "$kind" ]; then
echo "ERROR: ref '$ref' for '$name' is neither a tag nor a branch on $repo, and is not a hex commit SHA" >&2
exit 1
fi
case "$kind" in
commit)
echo " --> $name @ commit $ref ($repo)"
git init -q "$dest"
git -C "$dest" remote add origin "$repo"
if ! git -C "$dest" fetch --depth 1 origin "$ref" 2>/dev/null; then
git -C "$dest" fetch origin
fi
git -C "$dest" checkout -q --detach "$ref"
git -C "$dest" submodule update --init --recursive --depth 1
;;
tag|branch)
echo " --> $name @ $kind $ref ($repo)"
git clone --recursive --depth 1 --branch "$ref" "$repo" "$dest"
;;
esac
done
echo
echo "==> Stripping .git, .github, build caches and IDE noise from cloned modules"
find "$work/modules" -name '.git' -prune -exec rm -rf {} + 2>/dev/null || true
find "$work/modules" -type d -name '.github' -prune -exec rm -rf {} + 2>/dev/null || true
find "$work/modules" -name '.gitmodules' -delete 2>/dev/null || true
# Local CMake/Rust build outputs from prior `make` runs. Not in a
# fresh clone, so they have no business in a source tarball. Two-tier
# strip so we don't accidentally remove tracked sources:
#
# 1. Module-root `bin/` and `target/` only — these are the module's
# own output dirs (never tracked). Per-module explicit path so
# we don't recurse into `deps/readies/bin/` (vendored scripts,
# tracked in readies' own repo) or any nested target/ that might
# be tracked.
# 2. Anywhere — `*-release` / `*-debug` directories. These flavor
# suffixes only appear on build-output dirs (e.g.
# `bin/macos-arm64v8-release/`), never on source.
#
# `build/` is NOT in this list — redisearch ships tracked .cmake/Makefile
# recipes under `src/build/{boost,hiredis,libuv}/` that CMakeLists.txt
# `include`s at build time.
for m in $(manifest_modules); do
[ -d "$work/modules/$m/src/bin" ] && rm -rf "$work/modules/$m/src/bin"
[ -d "$work/modules/$m/src/target" ] && rm -rf "$work/modules/$m/src/target"
done
find "$work/modules" -type d \( -name '*-release' -o -name '*-debug' \) \
-prune -exec rm -rf {} + 2>/dev/null || true
# IDE-local config (Cursor, VSCode, JetBrains). Useless to a tarball consumer.
find "$work/modules" -type d \( -name .cursor -o -name .vscode -o -name .idea \) \
-prune -exec rm -rf {} + 2>/dev/null || true
# Vendored libraries' OWN benchmark suites and test datasets, found at
# */deps/*/{benchmarks,test_dataset,testdata}. These benchmark/test the
# bundled lib (e.g. fast_double_parser's Gay tests, ScalableVectorSearch's
# fvecs/svs corpora) — the module never references them at build or run
# time. Ship-only-what's-needed: ~35MB saved.
find "$work/modules" -type d \( -name benchmarks -o -name benchmark -o -name test_dataset -o -name testdata \) \
-path '*/deps/*' -prune -exec rm -rf {} + 2>/dev/null || true
# Module test fixtures — RDB backwards-compat snapshots and large input
# corpora (>1MB CSV/fvecs/svs/txt). Useful only when running the module
# test suite; this is a source/build tarball, not a test-dev kit.
find "$work/modules" -path '*/tests/*' -type f -name '*.rdb' -delete 2>/dev/null || true
find "$work/modules" -path '*/tests/*' -type f \
\( -name '*.csv' -o -name '*.fvecs' -o -name '*.svs' -o -name '*.txt' \) \
-size +1M -delete 2>/dev/null || true
echo "==> Marking modules pre-prepared (so consumer 'make build' skips clone step)"
for name in $(manifest_modules); do
touch "$work/modules/$name/src/.prepared"
done
echo
echo "==> Overlaying local config-sync tooling in staging"
# Overlay the LOCAL sync-redis-conf / apply-redis-conf scripts + their lib
# + the manifest on top of whatever came in from `git archive $TAG`, so
# tarball-time fixes (or the modules.yaml-into-modules/ relocation) don't
# require re-tagging Redis core. If $TAG predates that relocation, drop the
# stale root-level modules.yaml so the in-staging tooling has exactly one
# manifest to find.
mkdir -p "$work/scripts/lib" "$work/modules"
cp scripts/sync-redis-conf.sh "$work/scripts/sync-redis-conf.sh"
cp scripts/apply-redis-conf.sh "$work/scripts/apply-redis-conf.sh"
cp scripts/deploy.sh "$work/scripts/deploy.sh"
cp scripts/lib/manifest.sh "$work/scripts/lib/manifest.sh"
cp modules/modules.yaml "$work/modules/modules.yaml"
chmod +x "$work/scripts/sync-redis-conf.sh" "$work/scripts/apply-redis-conf.sh" "$work/scripts/deploy.sh"
rm -f "$work/modules.yaml"
echo
echo "==> Generating redis-full.conf and applying into redis.conf (ASSUME_BUILT=1)"
# Run apply-redis-conf inside the staging tree: generates redis-full.conf with
# relative loadmodule paths, then overwrites redis.conf with the result.
# ASSUME_BUILT=1 emits active loadmodule lines even though .so files aren't
# present yet — consumer builds first, then runs redis-server redis.conf.
( cd "$work" && ASSUME_BUILT=1 bash scripts/apply-redis-conf.sh )
echo
echo "==> Producing reproducible tarball at $out"
mtime="$(git log -1 --format=%ct "$TAG")"
if [ -z "$mtime" ]; then
echo "ERROR: could not resolve commit timestamp for TAG=$TAG" >&2
exit 1
fi
rm -f "$out"
# Stage the uncompressed tar to a temp file, then gzip it — same reason as
# above: without `pipefail`, a `tar | gzip` failure in tar would be hidden by
# gzip succeeding. Separating the steps lets `set -e` catch a tar failure.
_pkg_tar="$(mktemp)"
( cd "$staging" && "$TAR" \
--sort=name \
--mtime="@$mtime" \
--owner=0 --group=0 --numeric-owner \
-c "$prefix" ) > "$_pkg_tar"
gzip -n < "$_pkg_tar" > "$out"
rm -f "$_pkg_tar"
echo "==> Cleaning staging $staging"
rm -rf "$staging"
compute_sha256() {
local f="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$f" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$f" | awk '{print $1}'
else
openssl dgst -sha256 "$f" | awk '{print $NF}'
fi
}
echo
echo "==> Tarball ready: $out"
size="$(du -h "$out" | awk '{print $1}')"
sha="$(compute_sha256 "$out")"
echo " size: $size"
echo " sha256: $sha"