Matteo Benedetto 5f82187c67 progress: barra di avanzamento per download ed estrazione
Aggiunge src/progress.{c,h}: barra a una riga attiva solo quando stdout e' un
terminale (con output rediretto/pipeline non stampa nulla, quindi l'output resta
identico al client Python: parita' 30/30 invariata).

- due fasi: "Scaricamento" (byte da Content-Length) ed "Estrazione" (byte
  compressi consumati + membri processati: nuovo contatore in gz_reader e
  callback tar_progress_fn/tar_extract_cb)
- controllo con CELLAR_PROGRESS=auto|bar|plain|off (default auto)
- nessun codice ANSI, solo '\r' e riempimento con spazi; glifi ASCII se la
  locale non e' UTF-8, larghezza da TIOCGWINSZ e misurata in colonne (i glifi
  UTF-8 sono multi-byte), ridisegno throttled, velocita' a media mobile, ETA
- percorsi d'errore: progress_abort() chiude la riga senza riepilogo; il file
  parziale resta come nel client Python
- tests/progress_test.sh: pipe silenziosa, PTY via script (barra, ETA, riepilogo,
  nessun ANSI, righe entro la larghezza), modalita' plain e off, integrita' del
  file scaricato e della bottiglia installata (13/13 verdi)
- tests/parity_test.sh resta 30/30; mock_server.py con --throttle per i test
2026-09-20 17:46:29 +02:00

cellar-cli-c

Reimplementazione in C del client CLI di Cellar (cellar-cli.py), con l'obiettivo di un binario il più retrocompatibile possibile: statico, senza dipendenze, compilabile anche per i686 e aarch64 (handheld).

Autore: Matteo Benedetto — progetto derivato da enne2/cellar (commit f5216b1).

Stato

Comandi list, upload, download, install, scan-local, wizard-upload, wizard-install
Codice ~5.800 righe C99 (POSIX.1-2008), mono-thread
Dipendenze nessuna: miniz vendored (deflate/inflate), resto scritto a mano
Parità col client Python 30/30 test automatici (tests/parity_test.sh), output byte-identico
Build x86_64 static, i686 static, aarch64 static (cross)
Installazione locale ~/.local/bin/cellar-cli → symlink a dist/cellar-cli

Download dei binari pronti

Nella release v1.0.0 sono allegati tre binari statici (nessuna dipendenza a runtime):

file architettura ABI minima
cellar-cli-1.0.0-linux-x86_64-static x86-64 (amd64) Linux, ABI 3.2.0
cellar-cli-1.0.0-linux-i686-static i686 (x86 32 bit) Linux, ABI 3.2.0
cellar-cli-1.0.0-linux-aarch64-static aarch64 (arm64) Linux, ABI 3.7.0

Insieme a SHA256SUMS.txt per la verifica: sha256sum -c SHA256SUMS.txt.

Pubblicato su Gitea: https://git.enne2.net/enne2/cellar-cli-c/releases/tag/v1.0.0

Build

make            # dist/cellar-cli          (x86-64 statico, glibc)
make asan       # dist/cellar-cli-asan     (dinamico con ASan/UBSan, per i test)
make 32         # dist/cellar-cli-i686     (statico 32 bit)
make arm64      # dist/cellar-cli-aarch64  (cross toolchain ARM GNU 13.2, statico)
make dist       # tutti e tre
make verify     # analisi di portabilità del binario (vd. sotto)
make test       # test di parità contro il client Python (server mock inclusi)
make clean

Variabili utili: CC=..., AARCH64_CC=... (default ~/toolchains/arm-gnu-toolchain-13.2.Rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc).

Installazione nel PATH (utente, nessun root):

ln -sfn "$PWD/dist/cellar-cli" ~/.local/bin/cellar-cli

Configurazione

Stesso file del client Python: ~/.cellar.conf

[cellar]
server = http://brain.vpn:8080
bottles_dir = /home/enne2/.var/app/com.usebottles.bottles/data/bottles/bottles

Se il file non esiste viene creato con i default (e l'avviso su stderr), esattamente come fa configparser nel client Python. --server URL ha la precedenza.

Comandi

cellar-cli list                                   # elenca gli archivi sul server
cellar-cli upload Backup.tar.gz --name 'Gioco' --bottle-name 'Gioco' \
           --description '...' --tags 'gog,rpg' --arch win32 --runner soda-9.0-1 \
           --windows-version win10                # upload multipart (streaming)
cellar-cli download 3 ~/Downloads/                # nome file dal Content-Disposition
cellar-cli install 'Gioco' [--replace]            # scarica, estrae, installa nella dir Bottles
cellar-cli scan-local [--json]                    # bottiglie locali (legge bottle.yml)
cellar-cli wizard-upload | wizard-install         # flussi interattivi
cellar-cli --server http://10.8.0.3:8080 list     # override del server

Exit code: 0 ok, 1 errore HTTP/rete/file, 2 argomenti non validi (come argparse).

Barra di avanzamento

Durante install, wizard-install e download viene mostrata una barra su una riga, con due fasi (Scaricamento ed Estrazione):

Scaricamento [████████████░░░░░░░░░░░░]  58%  6.7 MB/11.5 MB  3.0 MB/s  ETA 00:01
Estrazione   [████████████████████████] 100%  11.5 MB/11.5 MB  178 MB/s  membri: 20
  • attiva solo se stdout è un terminale: con output rediretto, in pipeline, in cron o nei log non stampa nulla — per questo l'output resta identico al client Python;
  • controllo con la variabile d'ambiente CELLAR_PROGRESS:
    • auto (default) → barra su terminale, silenzio altrove;
    • bar → barra anche con output rediretto;
    • plain → una riga ogni 10% (per log e cron, senza disegno);
    • off → disattivata;
  • nessun codice ANSI: solo \r e riempimento con spazi, quindi funziona anche su console vecchie e seriali; glifi ASCII (#/-) se la locale non è UTF-8, blocchi Unicode (/) se lo è;
  • larghezza adattata al terminale (TIOCGWINSZ, fallback 80 colonne, max 200) e misurata in colonne, non in byte (i glifi UTF-8 sono multi-byte);
  • ridisegno limitato a ~8 volte al secondo, velocità con media mobile, ETA mostrata solo quando la stima è affidabile;
  • CELLAR_PROGRESS_DEBUG=1 stampa su stderr modalità/larghezza calcolate (diagnostica).
CELLAR_PROGRESS=plain cellar-cli install 'Gioco'   # output adatto a un log
CELLAR_PROGRESS=off   cellar-cli install 'Gioco'   # nessuna barra

Retrocompatibilità (le scelte che contano)

  • Binario statico (-static): nessuna dipendenza da glibc a runtime, quindi gira su distro vecchie e firmware con glibc più vecchia di quella di build. make verify riporta la nota ABI (for GNU/Linux 3.2.0) e l'assenza di simboli GLIBC_*.
  • Solo syscall classiche: open/read/write/stat/lstat/rename/mkdir/symlink/link/utimensat/chmod/poll. Nessun statx, openat2, O_TMPFILE, memfd_create, getrandom, close_range (verificato automaticamente da tests/verify_binary.sh).
  • Large File Support: -D_FILE_OFFSET_BITS=64 (le bottiglie Cellar arrivano a 1,3 GB).
  • Resolver di riserva: se getaddrinfo fallisce (tipico di una glibc statica senza NSS a runtime), si passa a /etc/hosts e poi a una query DNS diretta su UDP leggendo /etc/resolv.conf. Nessuna dipendenza da nss_dns/nss_files.
  • 32 bit e aarch64: -m32 per sistemi i686 (i vecchi handheld/console), cross aarch64 per muOS/RG40XXH; il binario è statico anche lì, quindi non eredita il problema "toolchain Debian 13 (glibc 2.41) vs firmware (glibc 2.38)".
  • gzip autonomo: miniz viene usato solo per deflate/inflate raw, mentre header e trailer gzip (CRC32 + ISIZE) sono gestiti a mano: la API mz_deflate/mz_inflate di miniz non supporta il contenitore gzip, e così non serve alcuna libreria zlib (nemmeno per il cross build aarch64).

Parità col client Python

tests/parity_test.sh avvia due server mock indipendenti (stesso stato iniziale) e confronta per ogni caso stdout, stderr ed exit code del binario C e di tests/ref/cellar-cli.py (copia verbatim del client originale). Copre: list, upload, download (su file e su directory), install (prima volta / esistente / --replace / ref inesistente), 404, server irraggiungibile, entrambi i wizard con input da pipe, sei casi di errore di argomenti, più verifiche sugli artefatti:

  • il download produce byte identici all'upload;
  • l'albero installato (tipo, permessi, dimensione, target dei symlink, contenuto, mtime) è identico tra i due client;
  • l'archivio creato dal writer C è equivalente a quello creato da tarfile (confronto membro per membro) ed è leggibile da Python e da GNU tar;
  • Python riesce a estrarre l'archivio scritto in C con metadata identici.

Differenze note e volute

Aspetto Comportamento
--help / usage Struttura e messaggi di errore di argparse riprodotti; il testo descrittivo del --help è più sintetico
Tar: mtime PAX Scritto con 7 decimali (Python usa repr() del float): differenza massima ~1e-7 s
Tar: mtime dei symlink in estrazione Non ripristinato, come tarfile (if not tarinfo.issym()): resta l'istante di estrazione. tests/compare_trees.py lo tiene presente
Tar: entry speciali (device) Saltate con warning su stderr invece di interrompere l'estrazione
Confronto nomi case-insensitive ASCII (tolower) invece di str.lower() Unicode
EOF su una richiesta interattiva Python solleva EOFError (exit 1); qui si stampa EOFError: EOF when reading a line e si esce 1. Nei prompt con default (nome archivio, descrizione…) su EOF si applica il default, così i wizard restano usabili da script
Upload multipart In streaming (il client Python legge l'intero archivio in RAM)
Barra di avanzamento Aggiunta (il CLI Python non stampa nulla durante download/estrazione). Attiva solo su terminale, quindi l'output rediretto resta identico all'originale
TLS Non supportato (niente OpenSSL): serve http://, oppure un reverse proxy TLS. Il server Cellar è HTTP
Timeout di rete Connect 15 s, I/O 300 s (urllib non ha timeout)
Redirect Seguiti come urllib.request: GET/HEAD su 301/302/303/307/308, POST→GET su 301/302/303

Test e verifica

make test                                    # parità + barra (build ASan)
C_BIN=dist/cellar-cli tests/parity_test.sh    # parità con il binario statico
C_BIN=dist/cellar-cli tests/progress_test.sh  # barra: pipe silenziosa, PTY, plain, off
make verify                                   # file/ldd/ABI/simboli GLIBC/syscall/smoke test

tests/progress_test.sh usa un server mock con throttling (--throttle byte/s) e script -qec per allocare un PTY: verifica che su pipe non ci sia alcun output di progresso, che su terminale compaiano le due fasi con ETA e riepilogo, che nessuna riga superi la larghezza del terminale e che non ci siano codici ANSI.

Struttura

src/common.{c,h}    errori, dynbuf, memoria, UTF-8, path, normalizzazione sicura
src/json.{c,h}      parser JSON minimale + escaper ensure_ascii
src/http.{c,h}      HTTP/1.1 su socket: URL, redirect, chunked, resolver di riserva
src/multipart.{c,h} multipart/form-data in streaming
src/gzip.{c,h}      flusso gzip (RFC1952) su deflate raw di miniz
src/tar.{c,h}       tar writer PAX/ustar (come tarfile) + extractor con controllo traversal
src/fsutil.{c,h}    mkdir -p, rm -r, copia ricorsiva, move con fallback EXDEV, temp dir
src/config.{c,h}    ~/.cellar.conf (INI in stile configparser)
src/bottle.{c,h}    record archivi, scansione bottiglie, bottle.yml, backup
src/ui.{c,h}        tabelle e JSON con larghezze in code point (come le f-string)
src/ops.{c,h}       list/upload/download/install
src/progress.{c,h}  barra di avanzamento (download + estrazione, env CELLAR_PROGRESS)
src/wizard.{c,h}    flussi interattivi
src/main.c          CLI e messaggi di errore in stile argparse
third_party/        miniz (unlicense/MIT) + licenza
tests/              mock server, bottiglia finta, parità, confronti, verifica binario

Licenza e crediti

Codice del progetto Cellar di Matteo Benedetto (enne2/cellar), di cui questa è una reimplementazione in C. third_party/miniz è distribuito con licenza unlicense/MIT (vd. third_party/miniz.LICENSE).

S
Description
Reimplementazione in C del client CLI di Cellar (enne2/cellar): binario statico, zero dipendenze, massima retrocompatibilita (x86-64/i686/aarch64)
Readme
24 MiB
Languages
C 75.6%
Python 15.8%
Shell 7.8%
Makefile 0.8%