Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

lunedì 25 settembre 2023

Ricomincio da DEB
come creare un Debian Package - pt.1

Gaetano: Chello ch'è stato è stato, basta! Ricomincio da tre!
Lello: Da zero.
Gaetano: Eh?
Lello: Da zero! Ricominci da zero!
GaetanoNossignore, ricomincio da... cioè, tre cose me so' riuscite ind'a vita, pecchè aggià perdere pure cheste?! Aggià ricominciare da zero? Da tre!

Dopo il tormentone del CAN bus (un articolo in ben 4 parti!) ho deciso di ricominciare da zero, anzi... Ricomincio da tre, come il grande Massimo Troisi nella sua opera prima capolavoro. Ricominciare in che senso? Nel senso che, almeno per questa volta, invece di parlare della mia (nostra) amata programmazione in C parleremo di un argomento collegato e un po' trascurato, che corrisponde alla domanda: "Ma dopo aver sviluppato una applicazione Linux come la distribuisco?". Ecco, ci sono vari metodi, ma oggi ho voglia di parlare del più elementare e facile da usare, e cioè il "pacchetto di distribuzione" e, in particolare, mi soffermerò sul tipo più usato, il Debian Package (DEB Package o Pacchetto Debian per gli amici) che si usa nelle distribuzioni Linux più diffuse (Ubuntu, Mint, Debian, etc.).

...ma perché devo ricominciare da zero? Io ricomincio da DEB!...

Come detto sopra, ci sono vari metodi per distribuire una applicazione Linux e, prima di parlare del Debian Package, bisogna fare alcune precisazioni: indubbiamente, il metodo più rigoroso di distribuzione è basato su autotool: si distribuisce tutto l'ambiente di sviluppo (sorgenti, makefile e file di configurazione), e quindi si compila e installa usando il classico comando "./configure && make install". Questo metodo è rigoroso perché si auto-adatta (grazie ad autotool) alla macchina destinazione su cui può essere in uso una qualsiasi versione di Linux, tanto l'applicazione viene compilata e linkata localmente con le risorse a disposizione.

Ma usare autotool è un metodo molto specialistico (credo sia evidente... è roba da programmatori ah ah ah) mentre gli utenti "normali"  vogliono installare e usare in quattro e quattr’otto, (e se gli utenti hanno un passato Windows non ne parliamo neanche...). E quindi anche nel mondo Linux si usano degli installer che si chiamano "pacchetti di distribuzione", e che sono facilissimi da usare ma sono un po' meno flessibili di autotool: non si adattano automaticamente alla macchina (contengono l' applicazione precompilata) e quindi in alcuni casi potrebbero non essere installabili al primo colpo: ad esempio è possibile che la applicazione da installare usi una libreria di una versione differente di quella già presente nella macchina, e questo genera la segnalazione di un errore. In generale, però, è sempre abbastanza facile risolvere eventuali problemi, se l'installer è ben fatto.

Ah, dimenticavo: ovviamente un Debian Package ha un doppio uso: installare e disinstallare (e anche questa seconda attività è importante, no?). E allora, come si crea un pacchetto di questo tipo? Vediamo prima di tutto come è fatto: il Debian Package è un file di tipo archivio compresso che, internamente, ha la seguente struttura tipica:

├── DEBIAN
│ ├── control
│ ├── preinst
│ ├── postinst
│ ├── prerm
│ └── postrm
├── etc
│ └── systemd
│ └── system
│ └── myservice.service
└── usr
└── bin
└── myapp

ossia: ci sono tre directory principali (DEBIAN, etc e usr) che hanno il seguente uso:

  • DEBIAN: contiene i file di controllo e di pre/post installazione/disinstallazione:
    • control: è il file di controllo master, quello che guida l'installazione, infatti è l'unico file obbligatorio.
    • preinst: è uno shell script preparatorio eseguito automaticamente prima dell'installazione vera e propria. È opzionale.
    • postinst: è uno shell script di finalizzazione eseguito automaticamente dopo l'installazione. È opzionale.
    • prerm: è uno shell script preparatorio eseguito automaticamente prima della disinstallazione vera e propria. È opzionale.
    • postrm: è uno shell script di finalizzazione eseguito automaticamente dopo la disinstallazione. È opzionale.
  • lib: contiene l'albero di directory che replica l'albero dei servizi systemd della macchina destino: in questo lib/systemd/system si copieranno i servizi (file con estensione .service) necessari alla nostra applicazione (questa directory lib è opzionale, serve solo se il Package installa anche dei servizi systemd).
  • usr: contiene l'albero di directory che replica l'albero delle applicazioni della macchina destino: in questo usr/bin si copieranno gli eseguibili che compongono la nostra applicazione (nell'esempio sopra è una sola applicazione che si chiama "myapp").

E come è fatto un file control? È un file di testo che contiene alcune linee che descrivono il Package, e le linee che, come minimo, devono essere presenti sono:

  • Package: il nome dell'applicazione da installare.
  • Version: la versione dell'applicazione da installare.
  • Maintainer – il nome e l'indirizzo email del responsabile del Package.
  • Description – una descrizione corta dell'applicazione. Di solito questa è l'ultima linea del control: sotto questa linea si può aggiungere una descrizione più lunga che deve, però, cominciare con uno spazio.

si possono aggiungere molte altre linee con funzioni particolari, in particolare due linee che non dovrebbero mai mancare sono queste (N.B.: in realtà la lista dei campi required/recommended/optional varia un po' tra un manuale e l'altro... diciamo che usando i quattro campi qui sopra più i due qui sotto non si dovrebbero avere problemi):

  • Architecture – la architettura dove può correre la applicazione (i.e.: all, oppure amd64 oppure i386, oppure sparc... e molte altre. all si usa per le installazioni compatibili con qualsiasi macchina).
  • Depends: descrive le librerie (con le rispettive versioni) indispensabili all'installazione.

La linea Depends è un poco particolare: ci sono vari metodi per cercare quali sono le librerie indispensabili alla nostra applicazione, e uno dei metodi più interessanti usa un meccanismo dello stesso ambiente di generazione del Package, però con alcune stranezze (la prima volta che l'ho usato mi è costato un po' farlo funzionare). Ma uno dei miei compiti è svelare i trucchi, no? E vediamoli! Bisogna creare un mini-albero secondario che somiglia a quello principale mostrato sopra, un mini-albero come questo:

├── debian
│ └── control
└── usr
└── bin
└── myapp

sempre supponendo che la applicazione si chiami "myapp". Notare che debian è in minuscolo e che il file control  deve contenere solo questa linea:

source: myapp

dopodiché,  posizionandosi nella root-directory del mini-albero , si può eseguire il comando dpkg-shlibdeps ottenendo un risultato di questo tipo:

aldo@Linux $ dpkg-shlibdeps -O usr/local/bin/myapp
dpkg-shlibdeps: Avviso: binaries to analyze should already be installed in their package's directory
shlibs:Depends=libc6 (>= 2.34)
aldo@Linux $

Il risultato è, quindi, la lista delle librerie indispensabili (nell'esempio sopra c'è solo: libc6 dalla ver.2.34 in su) che potremo aggiungere in DEBIAN/control nella linea Depends. Dopodiché il mini-albero che abbiamo usato possiamo cancellarlo (ha già esaurito il suo compito) e possiamo tornare a usare l'albero principale.

Dopo questa introduzione teorica è l'ora di passare a un caso pratico, no? Vedendo un caso reale molti dubbi vengono cancellati automaticamente (beh, almeno a me succede spesso). Visto che ce l'abbiamo fresco in mente e abbiamo già il codice disponibile negli ultimi articoli pubblicati, cercheremo di creare un Debian Package che installa nel sistema canrecv, che è un CAN Server (e scusate se ritiro fuori un argomento appena visto!).

Ah, fino a qui ho dato per scontato che tutti sappiano cos'è un servizio systemd (e magari ci tornerò in futuro con un articolo) comunque in questo caso è sufficiente sapere che è la maniera più "moderna" di assegnare compiti a Linux, compiti del tipo: "al boot avvia questo", "se si interrompe riavvialo", ecc. Prima si usavano altri metodi (non so se a tutti è familiare il vecchio e sempre valido "init" derivato da "SysVinit"), ma da qualche anno a questa parte si usa quasi sempre systemd per cui lo useremo anche noi.

E torniamo al punto: l'obiettivo è, quindi, che il nostro Debian Package installi queste cose nel sistema:

  • un eseguibile cansetup.sh
  • un eseguibile canrecv
  • un servizio systemd  cansetup.service che produce l'esecuzione del CAN setup al boot  del sistema
  • un servizio systemd  canrecv.service che produce l'avvio del Server al boot  del sistema

Il codice di canrecv (canrecv.c) l'ho già pubblicato e lo possiamo trovare qui: è un Server molto minimale (riceve un messaggio ed esce) però per questo semplice esempio va più che bene (compito a casa: modificare canrecv.c per ottenere un CAN Server che riceva messaggi in loop). Anche lo shell script  cansetup.sh lo abbiamo visto in quest'altro articolo, ed è indispensabile al funzionamento del Server, visto che prepara il dispositivo virtual CAN del sistema. Ci mancano solo i due servizi: questo è cansetup.service:

[Unit]
Description=setup del vcan0 device
After=network.target

[Service]
ExecStart=/usr/local/bin/cansetup.sh

[Install]
WantedBy=default.target
mentre questo è canrecv.service:
[Unit]
Description=avvia il processo canrecv
After=cansetup.service

[Service]
ExecStart=/usr/local/bin/canrecv

[Install]
WantedBy=default.target

Come potete vedere sono abbastanza semplici (oserei dire: auto-esplicativi) e, comunque, spiegare come funzionano non è l'argomento di questo articolo (ripeto la semi-promessa: magari ci tornerò in futuro). L'importante è lo scopo di questi servizi, e quello credo che sia abbastanza chiaro. Quindi non ci resta che:

  • costruire l'albero di installazione (facile!)
  • copiarci dentro eseguibili e servizi (facilissimo!)
  • scrivere il file control (si può fare...)
  • scrivere gli eventuali script di pre/post installazione (spoiler: in questo semplice esempio ci serve solo lo script postinst)

E qui potrei concludere l'articolo aggiungendo le parti appena elencate... ma qui viene fuori la mia (nostra) anima di programmatore: perché fare a mano le operazioni descritte quando si possono automatizzare? Non posso concludere un articolo senza aggiungere una minima parte di programmazione, no?  E allora, nella seconda parte che arriverà prossimamente (giuro), vi presenterò un utile shell script che ho scritto per creare e ricreare (a piacere) il nostro Debian Package: è facilmente adattabile ad ogni uso futuro ed è una buona base per qualsiasi tipo di applicazione che abbisogna di un installer. Quindi per il momento vi saluto e, come sempre, vi consiglio di non trattenere il respiro nell'attesa!

Ciao, e al prossimo post!