Negli ultimi giorni ho studiato MSBuild per automatizzare
la pubblicazione su un server Web (che supporta PHP & MySQL, alla
faccia di ASP.NET ) di un file ZIP contenente i sorgenti di
Fatturazione. Ne ho sempre parlato in modo un po' vago, per cui
in questo post voglio descrivere nel modo più specifico quello che ho fatto.
Introduzione
In linea di massima, lo script parte da
questi concetti:
- ho i sorgenti da qualche parte, che devo
prelevare
- copio i sorgenti trasferendoli da qualche altra
parte
- zippo i sorgenti copiati al punto (2)
- faccio l'upload del file zippato su un server FTP
Perchè ho messo in corsivo e in blu le locazioni dei sorgenti? E' presto detto. I
sorgenti al punto (1) possono stare - per adesso - in due tipi di posti
diversi: o una directory sul mio hard-disk (D:\Documenti\Visual Studio
2005\Projects\Fatturazione) oppure mantenuti da un source control (come il
Vault SourceGear). Per adesso, partiamo dal presupposto di avere la nostra
soluzione in una directory, che è il caso più semplice che
capita a tutti noi, poi vedremo come modificare in modo semplice lo
script per fare diversamente.
Quindi, dicevamo, dobbiamo copiare tutti i
files dalla directory di origine alla directory di destinazione,
dobbiamo zippare i sorgenti ed
infine dobbiamo uploadare i
files zippati. Contate i grassetti nella frase precedente e scoprirete quanti
task deve eseguire il nostro script per MSBuild: solo 3? In realtà, ce n'è uno in più, ovvero
quello iniziale che si occupa di cancellare gli
eventuali residui di una precedente esecuzione dello script
(cancellazione della directory di destinazione ed il file ZIP
vecchio.).
Detto questo, passiamo alla pratica.
Struttura dello script per MSBuild
Uno script di MSBuild
non è nient'altro che un file XML creato in modo opportuno, rispettando un certo
schema e così via. Vediamo la struttura di base.
<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
...
...
<Target Name="All">
<CallTarget Targets="DeleteLastDownload" />
<CallTarget Targets="CopyFiles" />
<CallTarget Targets="CreateZip" />
</Target>
</Project
Il tag Project comincia lo script. La proprietà
DefaultTargets indica il task che viene eseguito per primo: il
task si chiama All, che esegue poi uno alla volta gli altri
task che compongono lo script, ovvero: DeleteLastDownload,
CopyFiles e CreateZip. In questo momento, non
abbiamo ancora considerato l'upload via FTP, ne parleremo più avanti. I punti di
sospensione indicano un blocco XML contenente uno alla volta il codice relativo
a ciascun task.
Esaminiamoli nel dettaglio adesso uno alla volta.
Il task DeleteLastDownload
Questo task fa semplicemente
due cose: cancella la directory di destinazione (se esiste) e il file ZIP (se
esiste). Questo task ha senso solo se abbiamo eseguito lo script già
una volta e quindi abbiamo i residui di una precedente esecuzione, appunto. Se
la directory o il file non esistono, le corrispondenti operazioni vengono
skippate senza danni. Il codice è:
<Target Name="DeleteLastDownload">
<RemoveDir Directories="$(DownloadDir)"/>
<Delete Files="$(ZipFileName)"/>
</Target>
$(DownloadDir) e $(ZipFileName) sono due costanti: ne parleremo un'altra
volta.
Il task CopyFiles
Questo task copia i sorgenti da una
directory di origine ad un'altra di destinazione. Per farlo, sfrutta una copia
ricorsiva: come tutti sappiamo, una soluzione .NET è articolata in molte
directory, una innestata all'altra, ma noi con un semplice comando possiamo
fare il tutto senza problemi. Il codice è questo:
<Target Name="CopyFiles">
<Copy
SourceFiles="@(MySourceFiles)"
DestinationFiles="@(MySourceFiles->'$(DownloadDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
Non chiedetemi molti dettagli, perchè io stesso l'ho copiata & incollata dal sito MSDN. Anche in questo caso, $(MySourceFiles) e $(DownloadDir) fanno riferimento a costanti.
Il task CreateZip
Questo task - che non fa parte del
pacchetto standard di MSBuild - crea un file ZIP partendo da uno o più files. Il
comando, su cui sono impazzito parecchio , è il seguente:
<ItemGroup>
<ZipFiles Include="$(DownloadDir)\**\*.*" />
</ItemGroup>
<Target Name="CreateZip">
<Zip ZipFileName="$(ZipFileName)" WorkingDirectory="$(DownloadDir)" Files="@(ZipFiles)"/>
</Target>
Attenzione: il gruppo di files da zippare è indicato dal tag
ItemGroup. Se anche voi dovete fare una cosa del genere,
assicuratevi di metterlo assolutamente prima del task
CreateZip. Sto ancora indagando sulla questione, ma ho la netta sensazione che se nello script voi copiate dei
files (come in questo caso) e
quell'include viene "valutato" prima della copia, nello ZIP non vengono
inclusi i files copiati durante l'esecuzione dello script. Io
sono diventato matto, perchè continuavo a generare il file ZIP vuoto,
oppure che non corrispondeva al 100% ai files effettivamente contenuti nella
directory di destinazione.
E se devo usare Vault SourceGear?
Se
i sorgenti sono mantenuti su un source control, possiamo recuperarli attraverso
il client di Vault disponibile da linea di comando. In pratica, il task
CopyFiles non ci servirebbe più, ma verrebbe sostituiti da un
GetLatestVersion che concettualmente fa la stessa cosa
(fornisce i sorgenti aggiornati), ma li preleva fisicamente da un repository
raggiungibile via http. Il task è:
<Target Name="GetLastestVersion">
<Exec Command=' $(SourceGearClientPath) GETWILDCARD -makewritable -destpath $(DownloadDir) $(SourceGearPath) * -user $(SourceGearUsername) -password $(SourceGearPassword) -host $(SourceGearHost) -REPOSITORY "Default Repository" ' />
</Target>
Il tag Exec non fa altro che eseguire il comando
riportato nella proprietà Command del tag stesso. Utilizza
tutta una serie di costanti $(qualcosa) per rendere più manutenibile lo
script. Una cosa importante: il comando qui sopra non fa il check-out dei
sorgenti, ma chiede una semplice copia dei files. Il check-out non avrebbe
senso, dal momento che lo si fa solo che bisogna modificare un file, cosa che a
noi non serve.
Conclusioni
C'è ancora qualcosa da vedere, ma non ho
tempo: come definire le costanti e, soprattutto capire quello che non ho ancora
capito, ovvero che differenza c'è tra i tag ItemGroup e
PropertyGroup. Stay tuned!