#!/bin/sh # Instalador do Cialai Broto para Linux. # # curl -fsSL https://cialai.com.br/broto/install.sh | bash # # Debian e Ubuntu recebem o .deb, Fedora, RHEL e openSUSE o .rpm, e as demais distribuições o .tar.gz, que traz só # o serviço, sem a janela. Tudo vem de https://cialai.com.br/downloads/broto e é conferido pelo SHA256SUMS.txt do # mesmo espelho; com o minisign instalado, a assinatura desse arquivo também é conferida. O serviço só entra depois # que o próprio cialai-broto confere a assinatura do pacote com a chave pública fixada abaixo, o mesmo passo do # botão Instalar e iniciar serviço da janela. Ele roda no systemd do usuário, com os poderes da conta que rodou o script, root incluído. # Com um terminal disponível, o script termina pedindo o código gerado no Cialai e faz o pareamento. # BROTO_FORMAT=deb, rpm ou tar força um formato; num servidor sem tela, tar evita as bibliotecas da janela. # BROTO_BASE_URL aponta para outro espelho. # Tudo roda dentro de main, chamada na última linha, então um download cortado no meio não executa nada. set -eu # Chave pública Minisign do Broto, a mesma de https://cialai.com.br/downloads/broto/minisign.pub. BROTO_KEY="RWSzkmWDGvm79LExlPtKBJapXxh6v9G4i5C/0HSNaCjGQ0PExubl5ADp" # Onde o .deb e o .rpm guardam o serviço assinado, ao lado da janela. PACKAGED_SERVICE=/usr/lib/CialaiBroto/service say() { printf '%s\n' "$*"; } fail() { printf 'Instalador do Cialai Broto: %s\n' "$*" >&2; exit 1; } has() { command -v "$1" >/dev/null 2>&1; } fetch() { if has curl; then curl -fsSL --retry 3 -o "$2" "$1" || fail "não foi possível baixar $1" elif has wget; then wget -q -O "$2" "$1" || fail "não foi possível baixar $1" else fail "é preciso ter curl ou wget" fi } as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif has sudo; then sudo "$@" else fail "instale o sudo para o script instalar o pacote" fi } digest() { if has sha256sum; then sha256sum "$1" | awk '{ print $1 }' elif has shasum; then shasum -a 256 "$1" | awk '{ print $1 }' else fail "é preciso ter sha256sum ou shasum" fi } # A distribuição decide, não as ferramentas no PATH: o Arch também empacota apt, dpkg, dnf e zypper, e um .deb ou # .rpm instalado ali passa por fora do pacman. ID_LIKE traz as distribuições de origem, então Mint, Pop, Rocky e # Leap seguem a delas. pick_format() { if [ -n "${BROTO_FORMAT:-}" ]; then printf '%s' "$BROTO_FORMAT" return fi ids="" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 ids="$(. /etc/os-release && printf '%s %s' "${ID:-}" "${ID_LIKE:-}")" || ids="" fi case " $ids " in *" debian "* | *" ubuntu "*) if has apt-get && has dpkg; then printf deb; return; fi ;; *" fedora "* | *" rhel "* | *" centos "* | *" suse "* | *" opensuse "*) if has dnf || has yum || has zypper; then printf rpm; return; fi ;; esac printf tar } # O serviço roda no systemd do usuário, então tudo é conferido antes do download. check_system() { case "$(uname -s)" in Linux) ;; Darwin) fail "o Broto roda em Linux e Windows. No Mac, o Cialai é quem comanda: https://cialai.com.br/#baixar" ;; *) fail "este instalador é para Linux. No Windows, rode no PowerShell: irm https://cialai.com.br/broto/install.ps1 | iex" ;; esac case "$(uname -m)" in x86_64 | amd64) ;; *) fail "o Cialai Broto para Linux é publicado só para x86_64, e esta máquina é $(uname -m)" ;; esac has systemctl || fail "o Broto roda como serviço do systemd do usuário, e este sistema não tem systemctl" systemctl --user show-environment >/dev/null 2>&1 \ || fail "não achei o systemd do usuário. Entre com a sua conta pelo login gráfico ou por SSH e rode de novo" } # Com o minisign instalado, confere também a assinatura do SHA256SUMS.txt. Sem ele, vale a soma do espelho e, # mais adiante, a assinatura que o próprio cialai-broto confere antes de instalar o serviço. check_signature() { has minisign || return 0 fetch "$base_url/SHA256SUMS.txt.minisig" "$tmp/SHA256SUMS.txt.minisig" minisign -Vm "$tmp/SHA256SUMS.txt" -x "$tmp/SHA256SUMS.txt.minisig" -P "$BROTO_KEY" >/dev/null 2>&1 \ || fail "a assinatura do SHA256SUMS.txt não confere com a chave do Broto; nada foi instalado" say "Assinatura do SHA256SUMS.txt conferida" } # Baixa o arquivo, confere a soma e deixa o resultado em $tmp/$file. download_checked() { fetch "$base_url/latest.json" "$tmp/latest.json" version="$(sed -n 's/^ *"tag": *"broto-v\([^"]*\)".*/\1/p' "$tmp/latest.json" | head -n 1)" say "Baixando o Cialai Broto ${version:-mais recente}, $file" fetch "$base_url/SHA256SUMS.txt" "$tmp/SHA256SUMS.txt" check_signature fetch "$base_url/$file" "$tmp/$file" chmod 644 "$tmp/$file" expected="$(awk -v name="$file" '$2 == name { print $1 }' "$tmp/SHA256SUMS.txt")" [ -n "$expected" ] || fail "o SHA256SUMS.txt não tem $file" actual="$(digest "$tmp/$file")" [ "$expected" = "$actual" ] || fail "a soma de $file não confere; nada foi instalado" say "Soma SHA256 conferida" } # O .deb e o .rpm instalam a janela e o serviço assinado em $PACKAGED_SERVICE. install_package() { # O apt lê pacotes locais como o usuário _apt, que não entra numa pasta temporária privada. chmod 755 "$tmp" case "$format" in deb) as_root apt-get update -qq as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y "$tmp/$file" ;; rpm) if has dnf; then as_root dnf install -y "$tmp/$file" elif has yum; then as_root yum install -y "$tmp/$file" else as_root zypper --non-interactive install --allow-unsigned-rpm "$tmp/$file" fi ;; esac [ -x "$PACKAGED_SERVICE/cialai-broto" ] || fail "o pacote não trouxe o serviço em $PACKAGED_SERVICE" payload="$PACKAGED_SERVICE" } # O .tar.gz é o pacote assinado inteiro. Ele abre numa pasta nova porque o cialai-broto recusa arquivo a mais. unpack_payload() { payload="$tmp/payload" mkdir "$payload" tar -xzf "$tmp/$file" -C "$payload" || fail "não foi possível abrir $file" [ -x "$payload/cialai-broto" ] || fail "$file não tem o cialai-broto" } # current-path aponta para o executável da versão ativa; o atualizador troca o ponteiro a cada versão nova. installed_binary() { [ -s "$install_root/current-path" ] || return 1 IFS= read -r active <"$install_root/current-path" || return 1 [ -x "$active" ] } # O cialai-broto confere a assinatura e cada arquivo do pacote, copia a versão para a pasta privada e liga o # serviço com o timer de atualização. A saída dele só aparece se algo falhar. install_service() { say "Conferindo a assinatura do pacote e instalando o serviço" if ! "$payload/cialai-broto" instalar --payload "$payload" --key "$BROTO_KEY" >"$tmp/instalar.log" 2>&1; then cat "$tmp/instalar.log" >&2 fail "o serviço do Broto não foi instalado" fi installed_binary || fail "o serviço foi instalado, mas $install_root/current-path não apareceu" say "Serviço instalado e ligado" } # Um comando estável no PATH do usuário, que segue o ponteiro e por isso continua valendo depois das atualizações. install_command() { bin_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" mkdir -p "$bin_dir" pointer="$(printf '%s' "$install_root/current-path" | sed "s/'/'\\\\''/g")" cat >"$bin_dir/.cialai-broto.new" <&2; exit 1; } exec "\$binary" "\$@" EOF chmod 755 "$bin_dir/.cialai-broto.new" mv -f "$bin_dir/.cialai-broto.new" "$bin_dir/cialai-broto" broto="$bin_dir/cialai-broto" case ":$PATH:" in *":$bin_dir:"*) shown=cialai-broto ;; *) shown="$broto" ;; esac } # Lê um campo de texto do JSON de status, que o Go grava numa linha só e sem espaços. json_field() { printf '%s' "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" } pair_help() { say "" say "Para parear, gere o código no Cialai do computador que vai comandar, em Dispositivos, Brotos, Conectar novo Broto." say "Depois rode $shown parear, cole o código e aperte Enter." } # O serviço sobe em segundo plano; o socket de controle aparece alguns segundos depois do systemctl. wait_service() { waited=0 while [ "$waited" -lt 30 ]; do if "$broto" status >/dev/null 2>&1; then return 0; fi sleep 1 waited=$((waited + 1)) done return 1 } # Depois do código, o computador mostra 4 dígitos e pede aprovação; o Broto liga quando a aprovação chega. wait_approval() { waited=0 while [ "$waited" -lt 300 ]; do current="$("$broto" status 2>/dev/null || true)" case "$current" in *'"state":"connected"'*) say "Pronto. O Broto está ligado a ${host:-esse computador}." return 0 ;; *'"state":"disconnected"'*) reason="$(json_field "$current" error)" say "${reason:-O pareamento não foi concluído.} Gere um código novo e rode $shown parear." return 0 ;; esac sleep 2 waited=$((waited + 2)) done say "Ainda aguardando a aprovação. Acompanhe com $shown status." } # O código vai pela entrada padrão do cialai-broto, nunca como argumento. Sem terminal, como em CI, só explica. pair() { if ! (: /dev/null; then pair_help return 0 fi if ! wait_service; then say "O serviço do Broto ainda não respondeu. Confira com systemctl --user status cialai-broto." pair_help return 0 fi host="$(json_field "$("$broto" status 2>/dev/null || true)" host)" if [ -n "$host" ]; then say "Este Broto já está pareado com $host. Para trocar, rode $shown desconectar e depois $shown parear." return 0 fi say "" say "No Cialai do computador que vai comandar, abra Dispositivos, Brotos, Conectar novo Broto e copie o código." printf 'Cole o código e aperte Enter, ou só Enter para parear depois: ' >/dev/tty code="" IFS= read -r code &1)"; then say "$result" say "O código não foi aceito. Gere um novo no Cialai e rode $shown parear." return 0 fi digits="$(json_field "$result" digits)" host="$(json_field "$result" host)" say "Confira se ${host:-o computador} mostra os mesmos 4 dígitos, ${digits:-os da janela do Cialai}, e aprove por lá." wait_approval } # Sem sessão gráfica, o systemd encerra o serviço do usuário quando a última sessão termina, como ao sair do SSH. linger_hint() { [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] || return 0 has loginctl || return 0 [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || true)" != yes ] || return 0 say "Sem sessão gráfica, o Broto para quando você sai. Para mantê-lo ligado, rode loginctl enable-linger." } main() { check_system base_url="${BROTO_BASE_URL:-https://cialai.com.br/downloads/broto}" format="$(pick_format)" case "$format" in deb) file=CialaiBroto_amd64.deb ;; rpm) file=CialaiBroto_x86_64.rpm ;; tar) file=CialaiBroto_linux-x64.tar.gz ;; *) fail "BROTO_FORMAT $format desconhecido; use deb, rpm ou tar" ;; esac # O mesmo caminho do os.UserConfigDir do Go, que o cialai-broto usa para a instalação. install_root="${XDG_CONFIG_HOME:-$HOME/.config}/CialaiBroto/install" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM say "Cialai Broto, instalador para Linux" version="" # Com o serviço já instalado, ele se atualiza sozinho: o .tar.gz não precisa vir de novo, e o .deb ou o .rpm só # atualizam a janela. if installed_binary && [ "$format" = tar ]; then say "O serviço do Broto já está instalado e se atualiza sozinho a cada 6 horas" else download_checked case "$format" in tar) unpack_payload ;; *) install_package ;; esac if installed_binary; then say "O serviço do Broto já estava instalado e se atualiza sozinho a cada 6 horas" else install_service fi fi install_command say "" say "Cialai Broto ${version:+$version }instalado." [ "$format" = tar ] || say "A janela Cialai Broto fica no menu de aplicativos." say "Comandos: $shown status, pausar, retomar e desconectar." pair linger_hint } main "$@"