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
207 lines
11 KiB
Markdown
207 lines
11 KiB
Markdown
# cellar-cli-c
|
|
|
|
Reimplementazione in **C** del client CLI di [Cellar](https://git.enne2.net/enne2/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
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
ln -sfn "$PWD/dist/cellar-cli" ~/.local/bin/cellar-cli
|
|
```
|
|
|
|
## Configurazione
|
|
|
|
Stesso file del client Python: `~/.cellar.conf`
|
|
|
|
```ini
|
|
[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
|
|
|
|
```bash
|
|
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).
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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`).
|