install
Rama master · réplica de lectura del repositorio original.
#!/bin/sh
# torrcast: curl -fsSL https://torrcast.anysda.space | sh (или .../install - тот же файл)
#
# Бутстрап, а не установка: узнаёт у GitHub последнюю версию, тащит тарбол этой
# версии, сверяет sha256, распаковывает в mktemp -d и зовёт install.sh оттуда.
# Всё тело - внутри функций: оборванная закачка (curl режет на середине) ничего
# не выполнит, потому что не доедет до вызова main в конце файла.
#
# 🔴 TC-887. Загрузчик тут ОДИН, а входа в него два, и это не совпадение, а решение:
# `cast --upgrade` зовёт этот же файл (install.sh кладёт его в $PREFIX/install), а
# не второй такой же на питоне. Отличается только преддверие - названная версия, от
# которой идём, - а узнавание тега, закачка, сверка sha256, распаковка и передача
# install.sh общие обоим входам. Заведи обновление своей закачкой, и два тела разъедутся
# ровно там, где чинить будут только одно из них.
set -eu
BOOTSTRAP_URL="${TORRCAST_BOOTSTRAP_URL:-https://torrcast.anysda.space}"
GITHUB_WEB="${TORRCAST_GITHUB_WEB:-https://github.com}"
PROJECT_PATH="${TORRCAST_PROJECT_PATH:-anysda/torrcast}"
LANGUAGE="${TORRCAST_LANGUAGE:-en}"
#: Установленная версия, от которой идёт обновление. Непустое значение - это и есть
#: второй вход: работа начинается со сверки версий, а не со скачивания.
UPGRADE_FROM="${TORRCAST_UPGRADE_FROM:-}"
case "$LANGUAGE" in
en|ru) ;;
# 🔴 TC-887. Код 1, а НЕ 2. Двойка занята штатным исходом установщика - урезанным
# каталогом индексеров (EXIT_CATALOG_CUT в install.sh), - и `cast --upgrade` читает
# её как «поставилось, но беднее полного». Отдай отсюда двойку, и отказ разбора
# доехал бы до человека словом «обновлено», хотя не тронуто ничего.
*) printf 'error: TORRCAST_LANGUAGE must be en or ru\n' >&2; exit 1 ;;
esac
say() { if [ "$LANGUAGE" = ru ]; then printf '%s\n' "$2" >&2; else printf '%s\n' "$1" >&2; fi; }
fail() { say "error: $1" "ошибка: $2"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || fail "$1 is required but is not in PATH" "нужен $1, его нет в PATH"; }
# Не root - не падаем молча: под sudo перезапускаемся тем же одностроком, без него
# просим человека сделать это самому. TORRCAST_NO_ROOT - лазейка для песочницы, как
# в install.sh.
# `sudo -E` тут не годится: на части машин sudo собран без сохранения окружения, и
# `-E` тихо игнорируется (sudo молча предупреждает в stderr и правит по-своему), из-за
# чего TORRCAST_GITHUB_WEB/TORRCAST_PROJECT_PATH/TORRCAST_BOOTSTRAP_URL пропадали бы у
# перезапущенного процесса без единого слова. Поэтому переопределения тащим не через
# окружение, а явными аргументами позиционника, и экспортируем их уже внутри новой
# оболочки за трубой - так их видит и curl, и запущенный им sh.
become_root() {
[ -n "${TORRCAST_NO_ROOT:-}" ] && return 0
[ "$(id -u)" -eq 0 ] && return 0
# 🔴 Обновление под sudo НЕ перезапускается одностроком. Односторок - это установка
# с нуля: он потерял бы и версию, от которой идём, и заставку обновления, и человек
# получил бы вместо `cast --upgrade` полную переустановку, о которой не просил.
# Поэтому тут именно строка «повтори вот так», а не тихая подмена работы.
have_sudo=""
command -v sudo >/dev/null 2>&1 && have_sudo=1
if [ -n "$UPGRADE_FROM" ]; then
[ -n "$have_sudo" ] \
&& fail "root is required: sudo cast --upgrade" "нужен root: sudo cast --upgrade"
fail "root is required: run cast --upgrade as root" \
"нужен root: запусти cast --upgrade от root"
fi
if [ -n "$have_sudo" ]; then
say "not root - restarting through sudo" "не root - перезапуск через sudo"
# Язык через sudo сносим, только если он НАЗВАН в окружении однострока (TC-955):
# экспорт дефолта отсюда сбрасывал бы язык из живого конфига при каждом
# обновлении не-root'ом. Молчание обязано доехать до install.sh молчанием.
exec sudo sh -c '
export TORRCAST_BOOTSTRAP_URL="$1" TORRCAST_GITHUB_WEB="$2" TORRCAST_PROJECT_PATH="$3"
[ -z "$4" ] || export TORRCAST_LANGUAGE="$4"
curl -fsSL "$1" | sh
' _ "$BOOTSTRAP_URL" "$GITHUB_WEB" "$PROJECT_PATH" "${TORRCAST_LANGUAGE:-}"
fi
# Сюда доходят только там, где sudo нет вовсе: советовать его тут значило бы
# послать человека за командой, которой у него на машине не существует.
fail "root is required: run it as root: sh -c \"curl -fsSL $BOOTSTRAP_URL | sh\"" \
"нужен root: запусти от root: sh -c \"curl -fsSL $BOOTSTRAP_URL | sh\""
}
# Печатает тег последней версии в stdout. Спрашивается не API, а сам `/releases/latest`:
# он отвечает 302, и в Location стоит `/releases/tag/vX.Y.Z`. Так узнаётся версия без
# api.github.com, у которого анониму отведено 60 запросов в час на адрес - потолок,
# делящийся на всех за одним NAT, и упереться в него значило бы получить отказ установки
# на ровном месте. До первого релиза тот же адрес уводит на `/releases` без `/tag/`:
# релизов ещё нет - сказать об этом словами, а не сырым перенаправлением.
latest_version() {
loc="$(curl -fsS --connect-timeout 10 -o /dev/null -w '%{redirect_url}' \
"$GITHUB_WEB/$PROJECT_PATH/releases/latest" 2>/dev/null)" \
|| fail "could not reach $GITHUB_WEB/$PROJECT_PATH/releases/latest" \
"не достучаться до $GITHUB_WEB/$PROJECT_PATH/releases/latest"
case "$loc" in
*/releases/tag/*) ;;
"") fail "no redirect from $GITHUB_WEB/$PROJECT_PATH/releases/latest" \
"$GITHUB_WEB/$PROJECT_PATH/releases/latest не перенаправил никуда" ;;
*) fail "no releases yet (releases/latest leads to the release list) - come back after the first release" \
"релизов ещё нет (releases/latest ведёт на список релизов) - вернись, когда выйдет первый релиз" ;;
esac
ver="${loc##*/releases/tag/}"
[ -n "$ver" ] || fail "the redirect names no tag" "в перенаправлении нет тега"
printf '%s\n' "$ver"
}
main() {
need curl
need tar
need sha256sum
need bash
become_root
# Тег из GitHub - с ведущей v (v1.0.0): так он стоит в Location у releases/latest,
# и так же он стоит в пути к ассету. Имена самих файлов - БЕЗ v (release.sh срезает
# её через ${tag#v}), а имя внутри .sha256 обязано побайтово совпасть с локальным
# именем файла.
tag="$(latest_version)"
ver="${tag#v}"
# Имя продукта не переводится и не транслитерируется: torrcast на обоих языках.
if [ -n "$UPGRADE_FROM" ]; then
# 🔴 Совпало - работы нет ВОВСЕ: ни временного каталога, ни закачки, ни единого
# тронутого файла. Ровно этим `cast --upgrade` отличается от «переустановить и
# посмотреть, что выйдет»: у переустановки тот же исход стоит полного прогона
# установщика, перезаписи юнитов и перезапуска служб под идущим показом.
# Выход отсюда стоит ДО mktemp нарочно: так «не тронул ничего» держится кодом,
# а не обещанием прибрать за собой.
if [ "$ver" = "$UPGRADE_FROM" ]; then
say "torrcast $ver is already the latest version" \
"torrcast $ver - уже последняя версия"
return 0
fi
say "torrcast $UPGRADE_FROM → $ver" "torrcast $UPGRADE_FROM → $ver"
# Дальше по цепочке это читает install.sh из тарбола: он и рисует заставку
# обновления вместо заставки установки.
export TORRCAST_UPGRADE_FROM="$UPGRADE_FROM"
else
say "torrcast $ver" "torrcast $ver"
fi
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
pkg="$GITHUB_WEB/$PROJECT_PATH/releases/download/$tag"
curl -fsSL -o "$tmp/torrcast-$ver.tar.gz" "$pkg/torrcast-$ver.tar.gz" \
|| fail "could not download torrcast-$ver.tar.gz" "не скачался тарбол torrcast-$ver.tar.gz"
curl -fsSL -o "$tmp/torrcast-$ver.tar.gz.sha256" "$pkg/torrcast-$ver.tar.gz.sha256" \
|| fail "could not download the sha256 checksum" "не скачалась сверка sha256"
(cd "$tmp" && sha256sum -c "torrcast-$ver.tar.gz.sha256") >/dev/null \
|| fail "sha256 mismatch - the archive is damaged or was replaced" "sha256 не сошёлся - тарбол повреждён или подменён"
mkdir "$tmp/src"
tar -xzf "$tmp/torrcast-$ver.tar.gz" -C "$tmp/src"
[ -f "$tmp/src/install.sh" ] || fail "the archive has no install.sh" "в тарболе нет install.sh"
# install.sh - bash-скрипт со своим шебангом (#!/usr/bin/env bash), не sh: на
# Debian /bin/sh - dash, и на башизмах install.sh он падает сразу. </dev/null
# отцепляет stdin от недочитанного хвоста трубы curl | sh.
(cd "$tmp/src" && ./install.sh </dev/null)
exit $?
}
main "$@"