Distribuire un programma su Linux dovrebbe essere la parte facile del lavoro: si compila il codice, si prepara un archivio e lo si mette il file a disposizione degli utenti. In realtà, per chi sviluppa software destinato a più distribuzioni del pinguino, il packaging può trasformarsi in un secondo progetto da mantenere. È il problema raccontato dall’autore di Fresh, editor di testo e ambiente di sviluppo per terminale scritto in Rust, dopo aver provato praticamente tutte le strade disponibili: npm, Cargo, AppImage, Flatpak, pacchetti DEB e RPM, AUR, Nix, mise, Homebrew per Linux, Gentoo GURU e semplici archivi contenenti binari precompilati.
La difficoltà non nasce da una mancanza di strumenti. Semmai, al contrario, Linux ha sviluppato nel tempo molti modelli di distribuzione del software, spesso legati alla storia delle singole famiglie di distribuzioni: Debian e derivate ruotano attorno a DEB e APT, Fedora e Red Hat utilizzano RPM e DNF, Arch Linux ha i repository ufficiali affiancati da AUR. A questi si sono aggiunti formati indipendenti dalla distribuzione, gestori orientati agli sviluppatori e sistemi dichiarativi come Nix. Per un progetto relativamente piccolo, mantenerli tutti significa però moltiplicare build, metadati, procedure di pubblicazione e possibili punti di rottura.
Fresh offre un esempio particolarmente interessante perché il programma in sé è piccolo: l’autore indicava circa 12 MB da scaricare e circa 35 MB una volta estratto. Il tema, insomma, non riguarda la complessità dell’applicazione ma le difficoltà concrete nel consegnare lo stesso eseguibile a utenti Linux con configurazioni molto diverse.
Il vero problema non è creare il pacchetto, ma mantenerlo
Preparare una volta un file .deb o .rpm non è particolarmente difficile: mantenerne nel tempo una distribuzione affidabile è tutta un’altra musica. Cambiano dipendenze, tool di build, certificati, regole dei repository, versioni delle librerie e procedure di pubblicazione. Una nuova release dell’applicazione può quindi funzionare perfettamente e, allo stesso tempo, risultare non disponibile attraverso uno dei canali supportati.
Fresh ne ha sperimentato un caso concreto con mise. L’integrazione aveva funzionato senza problemi fino a quando GitHub modificò il materiale crittografico utilizzato dal proprio sistema di attestazione delle build. Una dipendenza di mise conservava una trust root non più aggiornata e la verifica smise di funzionare: il codice di Fresh non era cambiato, ma l’installazione risultava comunque rotta.
Un maintainer non controlla l’intera catena: registry, repository, strumenti di firma e package manager possono cambiare indipendentemente dal progetto che devono distribuire.
Perché Flatpak non è sempre la risposta giusta
Flatpak risolve molti problemi reali delle applicazioni desktop Linux: un’app può portare con sé buona parte dell’ambiente runtime richiesto e ottenere un comportamento molto più prevedibile su distribuzioni differenti. Inoltre il modello prevede una sandbox che limita l’accesso dell’applicazione al sistema host.
L’autore di Fresh racconta che proprio quest’ultimo aspetto rende Flatpak una scelta poco adatta per il suo software: un editor da terminale destinato anche allo sviluppo deve poter leggere progetti in numerose directory, eseguire programmi, aprire terminali, interagire con Git, raggiungere servizi di rete e lavorare con strumenti installati sull’host. Limitare queste operazioni significherebbe compromettere le funzioni principali del programma.
Flatpak permette naturalmente di ampliare i privilegi tramite le opzioni del manifest: si possono concedere accesso al filesystem, rete, socket e altre risorse. La documentazione ufficiale raccomanda però di mantenere tali autorizzazioni al minimo. Esiste persino un’interfaccia pensata per gli strumenti di sviluppo che permette a software fidato di eseguire processi fuori dalla sandbox; è utile per IDE come GNOME Builder, ma equivale ad attenuare il confine di isolamento.
Una soluzione come Flatpak non è sbagliata di per sé, anzi. Tuttavia un formato ottimo per un’app desktop autosufficiente non è necessariamente ideale per un tool di sviluppo che deve interagire liberamente con il sistema.
AppImage elimina alcuni problemi, ma ne introduce altri
Anche AppImage può in alcuni frangenti sembrare perfetto: si scarica un singolo file, lo si rende eseguibile e lo si avvia. Nessuna installazione tradizionale e nessun pacchetto specifico per Debian, Fedora o Arch.
Le AppImage moderne di tipo 2 contengono però un filesystem SquashFS: il runtime tenta normalmente di montarlo attraverso FUSE in una directory temporanea e avvia quindi il programma contenuto nell’immagine. È un meccanismo elegante, ma introduce sia una dipendenza da FUSE sia un costo durante l’avvio.
Per un editor che punta a comparire quasi istantaneamente sullo schermo, l’autore di Fresh considera tale latenza troppo elevata. La soluzione adottata è stata piuttosto drastica: estrarre una volta il contenuto dell’AppImage e utilizzare direttamente i file ottenuti, evitando il mount a ogni esecuzione.
La documentazione attuale del progetto indica che, per Fresh, questa modalità può rendere l’avvio circa 10 volte più rapido rispetto all’esecuzione diretta dell’AppImage.
Ma se l’immagine deve essere estratta e il programma può funzionare direttamente come binario, quanto serve ancora AppImage?
Il limite più insidioso si chiama glibc
Resta poi un problema che nessun contenitore di file risolve automaticamente: la compatibilità binaria con le librerie presenti sul sistema. Un programma compilato dinamicamente avvalendosi di una versione recente di glibc può richiedere simboli che una vecchia distribuzione non possiede. Copiare l’eseguibile su Ubuntu o Debian di qualche anno prima non garantisce quindi che parta.
È uno dei motivi per cui compilare Linux su una distribuzione recente e dichiarare semplicemente “binario universale” è rischioso. Il loader dinamico può interrompere l’avvio ancora prima che l’applicazione esegua la propria funzione main.
Una tecnica efficace consiste nel compilare su una distribuzione sufficientemente vecchia: il binario richiederà così una baseline di glibc precedente e funzionerà anche su sistemi nuovi, grazie alla compatibilità mantenuta dalla libreria. Ma significa costruire dentro immagini datate, gestire toolchain specifiche e fare attenzione alle componenti incluse nel processo di build. Non è esattamente la semplificazione che l’autore di un piccolo progetto cerca.
Fresh sceglie un binario statico e riduce la dipendenza dai package manager
La principale complicazione venuta a galla durante lo sviluppo di Fresh è semplice: supportare DEB, RPM, AUR, Flatpak, AppImage, Nix e altri canali significa mantenere procedure, metadati e infrastrutture differenti.
Un pacchetto .deb scaricato da GitHub, per esempio, non riceve automaticamente gli aggiornamenti tramite APT se non esiste anche un repository configurato; lo stesso vale per RPM e DNF. Anche AUR può introdurre dipendenze esterne: quando il servizio ha avuto problemi che impedivano la pubblicazione di nuovi pacchetti, alcune release di Fresh non hanno potuto seguire subito quel canale. Debian aggiunge poi requisiti rigorosi sulla gestione delle dipendenze, particolarmente impegnativi per progetti Rust con molte crate.
Fresh ha quindi scelto di usare un binario statico compilato con musl, disponibile per x86-64 e aarch64 e installabile in ~/.local senza privilegi di root.
Il linking statico riduce le incompatibilità legate alle diverse versioni di glibc e permette di distribuire un eseguibile molto più autonomo. Gli aggiornamenti possono essere gestiti direttamente dall’editor con fresh --cmd update: Fresh controlla checksum e attestazione della release prima di sostituire il binario.
I pacchetti tradizionali restano disponibili, ma il progetto evita così di affidare il percorso principale di installazione a una lunga catena di package manager e repository differenti.