ArchiveBox/bin/setup_monorepo.sh
2026-08-28 14:49:19 -07:00

338 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
GITHUB_BASE="${GITHUB_BASE:-https://github.com/ArchiveBox}"
MONOREPO_REMOTE="${MONOREPO_REMOTE:-$GITHUB_BASE/monorepo.git}"
REPO_NAMES=(abxbus abxpkg abx-plugins abx-dl archivebox)
GIT_BINARY=""
locked_abxpkg_spec() {
UV_LOCK_PATH="$1" uv run --no-cache --no-project python -c 'import os, tomllib; package = next(item for item in tomllib.load(open(os.environ["UV_LOCK_PATH"], "rb"))["package"] if item["name"] == "abxpkg"); wheel = package["wheels"][0]; print("abxpkg @ {}#{}".format(wheel["url"], wheel["hash"].replace(":", "=")))'
}
resolve_git_binary() {
local lock_file="$SCRIPT_REPO_ROOT/uv.lock"
if [[ ! -f "$lock_file" && -f "$SCRIPT_REPO_ROOT/archivebox/uv.lock" ]]; then
lock_file="$SCRIPT_REPO_ROOT/archivebox/uv.lock"
fi
local abxpkg_requirement="abxpkg"
if [[ -f "$lock_file" ]]; then
abxpkg_requirement="$(locked_abxpkg_spec "$lock_file")"
[[ -n "$abxpkg_requirement" ]] || { printf 'Unable to find abxpkg in %s\n' "$lock_file" >&2; exit 1; }
fi
export ABXPKG_LIB_DIR="${ABXPKG_LIB_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/archivebox/setup-monorepo-abxpkg}"
mkdir -p "$ABXPKG_LIB_DIR/env/bin"
uv run --no-cache --no-project --with "$abxpkg_requirement" abxpkg env \
--install \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env,brew,apt \
git >/dev/null
GIT_BINARY="$ABXPKG_LIB_DIR/env/bin/git"
test -L "$GIT_BINARY"
test -x "$GIT_BINARY"
}
repo_target_branch() {
case "$1" in
archivebox)
printf 'dev\n'
;;
*)
printf 'main\n'
;;
esac
}
is_member_repo() {
local repo_root="$1"
local repo_name
for repo_name in "${REPO_NAMES[@]}"; do
if [[ "$(basename "$repo_root")" == "$repo_name" ]]; then
return 0
fi
done
return 1
}
monorepo_remote_matches() {
case "$1" in
git@github.com:ArchiveBox/monorepo.git | \
git+ssh://git@github.com/ArchiveBox/monorepo.git | \
https://github.com/ArchiveBox/monorepo.git)
return 0
;;
*)
return 1
;;
esac
}
bootstrap_build_dependencies() {
export ABXPKG_LIB_DIR="$ROOT_DIR/.venv/abxpkg"
mkdir -p "$ABXPKG_LIB_DIR/env/bin"
export PATH="$ABXPKG_LIB_DIR/env/bin:$PATH"
case "$OSTYPE" in
linux*)
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env,apt \
--overrides='{"apt":{"install_args":["build-essential"]}}' \
cc >/dev/null
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env,apt \
--overrides='{"env":{"version":["ldapsearch","-VV"]},"apt":{"install_args":["ldap-utils","python3-dev","python3-setuptools","libssl-dev","libldap2-dev","libsasl2-dev","zlib1g-dev","libatomic1"],"version":["ldapsearch","-VV"]}}' \
ldapsearch >/dev/null
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env \
cc >/dev/null
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env \
--overrides='{"env":{"version":["ldapsearch","-VV"]}}' \
ldapsearch >/dev/null
test -L "$ABXPKG_LIB_DIR/env/bin/cc"
test -x "$ABXPKG_LIB_DIR/env/bin/cc"
test -L "$ABXPKG_LIB_DIR/env/bin/ldapsearch"
test -x "$ABXPKG_LIB_DIR/env/bin/ldapsearch"
;;
darwin*)
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env \
brew >/dev/null
local brew_binary="$ABXPKG_LIB_DIR/env/bin/brew"
local brew_target
local brew_root
brew_target="$(readlink "$brew_binary")"
test -x "$brew_target"
brew_root="$(dirname "$(dirname "$brew_target")")"
export ABXPKG_BREW_ROOT="$brew_root"
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env,brew \
--overrides='{"brew":{"install_args":["llvm"]}}' \
clang >/dev/null
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env,brew \
--overrides='{"env":{"version":["ldapvc","-VV"]},"brew":{"install_args":["openldap"],"postinstall_scripts":true,"version":["ldapvc","-VV"]}}' \
ldapvc >/dev/null
PATH="$PATH:$brew_root/opt/llvm/bin" \
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env \
clang >/dev/null
PATH="$PATH:$brew_root/opt/openldap/bin" \
uv tool run --from "$ROOT_DIR/abxpkg" abxpkg env \
--install \
--no-cache \
--json \
--lib="$ABXPKG_LIB_DIR" \
--binproviders=env \
--overrides='{"env":{"version":["ldapvc","-VV"]}}' \
ldapvc >/dev/null
test -L "$ABXPKG_LIB_DIR/env/bin/clang"
test -x "$ABXPKG_LIB_DIR/env/bin/clang"
test -L "$ABXPKG_LIB_DIR/env/bin/ldapvc"
local ldapvc_target
local next_ldapvc_target
local openldap_prefix
ldapvc_target="$(readlink "$ABXPKG_LIB_DIR/env/bin/ldapvc")"
while [[ -L "$ldapvc_target" ]]; do
next_ldapvc_target="$(readlink "$ldapvc_target")"
if [[ "$next_ldapvc_target" == /* ]]; then
ldapvc_target="$next_ldapvc_target"
else
ldapvc_target="$(dirname "$ldapvc_target")/$next_ldapvc_target"
fi
done
test -x "$ldapvc_target"
openldap_prefix="$(dirname "$(dirname "$ldapvc_target")")"
test -f "$openldap_prefix/include/ldap.h"
test -f "$openldap_prefix/lib/libldap.dylib"
export CPPFLAGS="-I$openldap_prefix/include${CPPFLAGS:+ $CPPFLAGS}"
export LDFLAGS="-L$openldap_prefix/lib${LDFLAGS:+ $LDFLAGS}"
export PKG_CONFIG_PATH="$openldap_prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
;;
*)
printf 'Unsupported monorepo setup platform: %s\n' "$OSTYPE" >&2
exit 1
;;
esac
}
sync_workspace() {
uv sync --all-packages --all-extras --all-groups --no-cache --active
}
ensure_setup_link() {
local repo_name="$1"
local repo_dir="$ROOT_DIR/$repo_name"
local link_path="$repo_dir/bin/setup_monorepo.sh"
local source_path="$ROOT_DIR/bin/setup.sh"
mkdir -p "$repo_dir/bin"
if [[ -e "$link_path" ]] && [[ "$source_path" -ef "$link_path" ]]; then
return
fi
if [[ -d "$link_path" && ! -L "$link_path" ]]; then
printf 'Refusing to replace directory: %s\n' "$link_path" >&2
exit 1
fi
rm -f "$link_path"
ln "$source_path" "$link_path"
}
bootstrap_monorepo_root() {
local monorepo_root="$1"
local origin_url=""
if [[ -d "$monorepo_root/.git" ]]; then
origin_url="$("$GIT_BINARY" -C "$monorepo_root" remote get-url origin 2>/dev/null || true)"
if [[ -n "$origin_url" ]] && ! monorepo_remote_matches "$origin_url"; then
printf 'Refusing to reuse existing git repo at %s (origin: %s)\n' "$monorepo_root" "$origin_url" >&2
exit 1
fi
if [[ -z "$origin_url" ]]; then
"$GIT_BINARY" -C "$monorepo_root" remote add origin "$MONOREPO_REMOTE"
fi
printf 'Updating monorepo root: %s\n' "$monorepo_root"
if "$GIT_BINARY" -C "$monorepo_root" -c pull.rebase=false pull --ff-only --quiet >/dev/null 2>&1; then
printf 'Updated monorepo root\n'
else
printf 'Skipping monorepo pull (local changes, divergent branch, detached HEAD, or no upstream)\n' >&2
fi
return
fi
printf 'Bootstrapping monorepo root in %s\n' "$monorepo_root"
"$GIT_BINARY" -C "$monorepo_root" init -b main >/dev/null
"$GIT_BINARY" -C "$monorepo_root" remote add origin "$MONOREPO_REMOTE"
"$GIT_BINARY" -C "$monorepo_root" fetch --depth=1 origin main --quiet
if "$GIT_BINARY" -C "$monorepo_root" checkout -B main --track origin/main >/dev/null 2>&1; then
printf 'Initialized monorepo root\n'
else
printf 'Failed to materialize monorepo root in %s; existing files likely conflict with tracked monorepo files\n' "$monorepo_root" >&2
exit 1
fi
}
resolve_git_binary
if is_member_repo "$SCRIPT_REPO_ROOT"; then
ROOT_DIR="$(cd -- "$SCRIPT_REPO_ROOT/.." && pwd)"
bootstrap_monorepo_root "$ROOT_DIR"
elif [[ -f "$SCRIPT_REPO_ROOT/pyproject.toml" ]]; then
ROOT_DIR="$SCRIPT_REPO_ROOT"
else
printf 'Unable to infer monorepo root from script location: %s\n' "$SCRIPT_DIR" >&2
exit 1
fi
ensure_member_repo() {
local repo_name="$1"
local repo_dir="$ROOT_DIR/$repo_name"
local target_branch
local current_branch
target_branch="$(repo_target_branch "$repo_name")"
if [[ -d "$repo_dir/.git" ]]; then
printf 'Updating existing checkout: %s\n' "$repo_name"
current_branch="$("$GIT_BINARY" -C "$repo_dir" branch --show-current)"
if [[ "$current_branch" != "$target_branch" ]]; then
"$GIT_BINARY" -C "$repo_dir" fetch --depth=1 origin "$target_branch" --quiet
"$GIT_BINARY" -C "$repo_dir" checkout -B "$target_branch" --track "origin/$target_branch" >/dev/null
fi
if "$GIT_BINARY" -C "$repo_dir" -c pull.rebase=false pull --ff-only --quiet origin "$target_branch" >/dev/null 2>&1; then
printf 'Updated: %s\n' "$repo_name"
else
printf 'Skipping pull for %s (local changes, divergent branch, detached HEAD, or no upstream)\n' "$repo_name" >&2
fi
return
fi
if [[ -e "$repo_dir" ]]; then
printf 'Refusing to overwrite existing path: %s\n' "$repo_dir" >&2
exit 1
fi
printf 'Cloning %s/%s.git -> %s\n' "$GITHUB_BASE" "$repo_name" "$repo_name"
"$GIT_BINARY" clone --branch "$target_branch" "$GITHUB_BASE/$repo_name.git" "$repo_dir"
}
for repo_name in "${REPO_NAMES[@]}"; do
ensure_member_repo "$repo_name"
done
for repo_name in "${REPO_NAMES[@]}"; do
ensure_setup_link "$repo_name"
done
cd "$ROOT_DIR"
deactivate || true
rm -Rf ./*/.venv # delete all sub-repo venvs, the monorepo venv needs to take precedence
uv venv --allow-existing "$ROOT_DIR/.venv"
# shellcheck disable=SC1091
source "$ROOT_DIR/.venv/bin/activate"
bootstrap_build_dependencies
sync_workspace
echo
echo
echo "[√] Monorepo setup complete, cloned and pulled: ${REPO_NAMES[*]}"
echo " MONOREPO_ROOT=$ROOT_DIR"
echo " VIRTUAL_ENV=$VIRTUAL_ENV"
echo " PYTHON_BIN=$VIRTUAL_ENV/bin/python"
echo
echo "TIPS:"
echo " - Use 'uv run ...' inside member repos for package work; the root uv.lock owns this workspace"
echo " - Always read $ROOT_DIR/README.md into context before starting any work"