Press "Enter" to skip to content

InnoSetupFilesGen – Automatizzare le operazioni manuali.

InnoSetupFilesGen, è una piccola utility per automatizzare la creazione della lista dei files di una applicazione per il compilatore di InnoSetup, questa operazione effettuata manualmente potrebbe portare ad errori, a dimenticarsi qualche file, a dimenticarsi di aggiornare un file quando si aggiunge un reference nuovo, pertanto automatizzarla ci aiuterà ad evitare errori non voluti quando creiamo un setup.

Si tratta di una applicazione molto semplice. Una applicazione Console a cui passare una serie di parametri che le permettono di produrre uno script con la lista dei files che compongono il setup di una applicazione.

IMPORTANTE!

La lista dei files referenziati viene composta leggendo il file .csproj del progetto di Visual Studio, pertanto è indispensabile che il progetto che produce il file eseguibile abbia al suo interno le referenze a TUTTE le .dll che non sono parte del framework compresi gli eventuali componenti in GAC, ecco perché se decidete di usare InnoSetup, è indispensabile che indichiate alle references del progetto che produce il vostro .Exe, il parametro CopyAlways = True.

help_page[6]

La finestra console con l’help della nostra applicazione, prima di iniziare a vedere come è fatta.

La Solution

Per generare la nostra nuova solution, utilizziamo l’applicazione Console standard di Visual Studio 2015, per creare il necessario a produrre la lista dei file che compongono una applicazione nel formato di InnoSetup, creeremo 5 classi:

  1. Arguments.cs – classe che rappresenterà un argomento da linea di comando da passare alle classi che processano i dati.
  2. ReferenceItem.cs – classe che rappresenterà i dati per uno dei files dell’applicazione e i metodi necessari a produrre il file nel formato richiesto da InnoSetup.
  3. ReferenceItemsCollection.cs – collection che conterrà tutti i dati dei files e il necessario per emettere lo script.
  4. CsProjParser.cs – classe helper che leggerà il file di progetto dell’applicazione e andrà a cercare tutti i file che la compongono.
  5. Program.cs – contiene il main che esegue la generazione del file di script.

solution_01[6]

La soluzione della nostra applicazione, vediamo ora ciascuna classe e cosa contiene.

Arguments.cs

In questa classe, sono ospitate in realtà due classi, una che rappresenta un argomento, l’altra è l’enumerazione che ci permette di decidere la verbosità in output della nostra applicazione console.

public enum Verbosity : int
{

    none = 0,
 
    minimum,
 
    all
}

I tre valori di verbosità ammessi dall’applicazione sono:

  • none = nessun messaggio
  • minimum = start e successo o errore
  • all= output completo
public class Arguments
{
 
    public string CsProject
    {
        get;
        set;
    }
 

    public string OutputDirectory
    {
        get;
        set;
    }
 

    public string OutputFileName
    {
        get;
        set;
    }
 
    public Verbosity Verbose
    {
        get;
        set;
    }
}

La classe che conterrà i valori degli argomenti passati all’applicazione, non dovendo avere interazioni con una User Interface, questa classe è molto semplice e non implementa l’evento property changed o le variabili a livello di classe, solo lo standard C# getter e setter. Vediamo cosa rappresentano le property:

  • CsProject – Nome del file di progetto dell’applicazione.
  • OutputDirectory – Directory del progetto che contiene i file che dovranno essere inseriti nel setup attenzione che il path deve essere relativo alla cartella su cui si trova il file di progetto.
  • OutputFileName – Nome del file di output dove mettere lo script, questo file deve essere sulla cartella del progetto oppure su una cartella ad esso relativa che deve essere già stata generata e indicata nel nome file.
  • Verbose – Il codice che stabilisce la verbosità dell’applicazione console.

ReferenceItem.cs

Questa classe, rappresenta i dati che definiscono uno dei file da inserire nel setup dell’applicazione.

public class ReferenceItem
{
...
}

Anche in questo caso si tratta di una classe molto semplice, senza alcun interfaccia o altri dati necessari alle classi simili che utilizziamo nelle applicazioni con finestre.

public string DestinationPath
{
    get;
    set;
}
 

public string FullName
{
    get;
    set;
}
 
public bool IsSystem
{
    get
    {
        return (FullName.Contains("Microsoft") || FullName.Contains("System") || FullName.Contains("mscorlib"));
    }
}
 
public string Path
{
    get;
    set;
}

Le quattro property che vengono inizializzate dal parser del progetto:

  • DestinationPath – contiene il path di destinazione del file sulla cartella di installazione è indispensabile se il nostro progetto ha dei dati posti in sottocartelle (ad esempio le resources di traduzione dell’interfaccia) oppure dei file accessori come immagini, documenti ed altro.
  • FullName – contiene il nome del file
  • IsSystem – verifica se il file appartiene a quelli che sono i file che fanno parte del framework.net (abbiamo preso quelli più usati, se necessario è opportuno emendare questo dato).
  • Path – contiene il path del file come definito nell’Assembly.
public string ToInnoSetupReferenceString()
{
    string ret = null;
    if (this.Path == null || this.Path.Trim().Length == 0)
    {
        ret = string.Format(";*** Path not found for assembly {0} ***", this.FullName);
    }
    else
    {
        //string pf = Environment.GetEnvironmentVariable("ProgramW6432");
        //string pf32 = Environment.GetEnvironmentVariable("ProgramFiles(X86)");
        //string realPath = this.Path.Replace(pf32, "{#pf32}");
        //realPath = realPath.Replace(pf, "{#pf}");
        string destPath = "";
        if (DestinationPath != null && DestinationPath.Trim().Length > 0)
        {
            if (!DestinationPath.StartsWith("\\"))
            {
                destPath = "\\" + DestinationPath;
            }
            else
            {
                destPath = DestinationPath;
            }
        }
        ret = (string.Format("Source: \"{0}\\{1}\"; DestDir: {{app}}{2}; Flags: IgnoreVersion;", Path, FullName, destPath));
    }
    return (ret);
}

Il metodo che converte il contenuto di una di queste classi nella riga dello script InnoSetup che serve a indicarlo come parte del prodotto finale.

public override string ToString()
{
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("Path: {0}", Path);
    sb.AppendLine();
    sb.AppendFormat("FullName: {0}", FullName);
    return (sb.ToString());
}

Ai fini del debug, ho anche predisposto il ToString.

ReferenceItemsCollection.cs

Questa classe è la collection che raccoglierà tutte le informazioni sui file e fornirà i metodi necessari a generare il file dello script.

public class ReferenceItemsCollection : List<ReferenceItem>
{
....
}

Anche in questo caso, utilizziamo una collection generica semplice come classe base, perché non dobbiamo interagire con la User Interface.

public ReferenceItem this[string fullName, string destinationPath]
{
    get
    {
        return (this.FirstOrDefault(item => item.FullName == fullName && item.DestinationPath == destinationPath));
    }
}
 
public bool ContainsName(string fullName, string destinationPath)
{
    return (this[fullName, destinationPath] != null);
}

Due metodi che hanno lo scopo di permettere di trovare se un elemento è già stato inserito nella collection prima di aggiungerlo.

public string ToInnoSetupString()
{
    StringBuilder sbNormal = new StringBuilder();
    StringBuilder sbSystem = new StringBuilder();
    foreach (ReferenceItem item in this)
    {
        if (!item.IsSystem)
        {
            sbNormal.AppendLine(item.ToInnoSetupReferenceString());
        }
        else
        {
            sbSystem.AppendLine(item.ToInnoSetupReferenceString());
        }
    }
    sbNormal.AppendLine();
    sbNormal.AppendLine(sbSystem.ToString());
    return (sbNormal.ToString());
}

Il metodo qui sopra, produce una stringa che contiene lo script completo che sarà salvato su file, nel formato previsto da InnoSetup.

public override string ToString()
{
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < this.Count; i++)
    {
        sb.AppendLine(this[i].ToString());
    }
    return (sb.ToString());
}

Per il debug, implementiamo anche il metodo che visualizza il contenuto della collection su una stringa.

CsProjParser.cs

E’ la classe più importante e complessa di questa semplice utility, infatti legge il contenuto delle cartelle di output e quello del file XML del progetto e cerca e compone tutti i nomi dei file di progetto.

public class CsProjParser
{
......
} 

Anche in questo caso è una classe molto semplice, un helper che contiene il necessario a leggere dal file di progetto le informazioni che ci interessano:

private const string AssemblyReferencesXPath = "/p:Project/p:ItemGroup/p:Reference[count(p:HintPath)=1]";
private const string ContentReferencesXPath = "/p:Project/p:ItemGroup/p:Content[count(p:CopyToOutputDirectory)=1]";
private const string ProjectFileExtension = "csproj";
private const string XmlNameSpace = @"http://schemas.microsoft.com/developer/msbuild/2003";
private const string XmlNameSpacePrefix = "p";

Le costanti qui sopra definite, ci permettono di cercare le porzioni dell’XML di progetto che ci servono per leggerne il contenuto.

public CsProjParser(string outputRelativePath, string csProjFileName)
{
    this.OutputRelativePath = outputRelativePath;
    this.CsProjFileName = csProjFileName;
    References = new ReferenceItemsCollection();
}

Il costruttore, che richiede il path relativo dove trovare fisicamente i file, e il nome del file di progetto e genera la collection ove saranno inseriti i dati dei file.

public string CsProjFileName
{
    get;
    private set;
}
 

public string OutputRelativePath
{
    get;
    private set;
}
 

public ReferenceItemsCollection References
{
    get;
    private set;
}

Le tre property utilizzate dal parser per il suo funzionamento.

public void GenerateReferences()
{
    References.Clear();
 
    string[] fileNames = Directory.GetFiles(OutputRelativePath, "*.*", SearchOption.AllDirectories);
    for (int i = 0; i < fileNames.Length; i++)
    {
        string name = Path.GetFileName(fileNames[i]);
        string destpath = Path.GetDirectoryName(fileNames[i].ToLower().Replace(OutputRelativePath.ToLower(), ""));
        if (name.ToLower().EndsWith(".pdb", System.StringComparison.OrdinalIgnoreCase)) continue;
        if (name.ToLower().EndsWith(".vshost.exe", System.StringComparison.OrdinalIgnoreCase)) continue;
        if (!References.ContainsName(name, destpath))
        {
            ReferenceItem item = new ReferenceItem();
            item.DestinationPath = destpath;
            item.Path = Path.GetDirectoryName(fileNames[i]);
            item.FullName = name;
            References.Add(item);
        }
    }
 
    this.xmlDocument = new XmlDocument();
    this.xmlDocument.Load(CsProjFileName);
    this.xmlNamespaceManager = new XmlNamespaceManager(this.xmlDocument.NameTable);
    this.xmlNamespaceManager.AddNamespace(XmlNameSpacePrefix, XmlNameSpace);
    XmlNodeList list = xmlDocument.SelectNodes(AssemblyReferencesXPath, xmlNamespaceManager);
    foreach (XmlNode node in list)
    {
        foreach (XmlNode cnode in node.ChildNodes)
        {
            if (cnode.Name.Equals("HintPath", System.StringComparison.OrdinalIgnoreCase))
            {
                string name = Path.GetFileName(cnode.InnerText);
                if (References.ContainsName(name, "")) continue;
                ReferenceItem item = new ReferenceItem();
                item.Path = Path.GetDirectoryName(cnode.InnerText);
                item.FullName = name;
                References.Add(item);
            }
        }
    }
 
    list = xmlDocument.SelectNodes(ContentReferencesXPath, xmlNamespaceManager);
    foreach (XmlNode node in list)
    {
        string filePath = node.Attributes["Include"].InnerText;
 
        bool isLink = false;
        foreach (XmlNode cnode in node.ChildNodes)
        {
            if (cnode.Name.Equals("link", System.StringComparison.OrdinalIgnoreCase))
            {
                isLink = true;
                break;
            }
        }
 
        ReferenceItem item = new ReferenceItem();
        string name = Path.GetFileName(filePath);
        string destpath = Path.GetDirectoryName(filePath).Replace(OutputRelativePath, "");
        if (References.ContainsName(name, destpath)) continue;
        if (!isLink)
        {
            item.DestinationPath = destpath;
 
            item.Path = Path.Combine(OutputRelativePath, Path.GetDirectoryName(filePath));
        }
        else
        {
            item.Path = Path.GetDirectoryName(filePath);
        }
        item.FullName = name;
        References.Add(item);
    }
}

Il metodo che legge i dati delle cartelle di output e il file di progetto, vediamolo per pezzi spiegando che cosa fa esattamente:

References.Clear();
 
string[] fileNames = Directory.GetFiles(OutputRelativePath, "*.*", SearchOption.AllDirectories);
for (int i = 0; i < fileNames.Length; i++)
{
    string name = Path.GetFileName(fileNames[i]);
    string destpath = Path.GetDirectoryName(fileNames[i].ToLower().Replace(OutputRelativePath.ToLower(), ""));
    if (name.ToLower().EndsWith(".pdb", System.StringComparison.OrdinalIgnoreCase)) continue;
    if (name.ToLower().EndsWith(".vshost.exe", System.StringComparison.OrdinalIgnoreCase)) continue;
    if (!References.ContainsName(name, destpath))
    {
        ReferenceItem item = new ReferenceItem();
        item.DestinationPath = destpath;
        item.Path = Path.GetDirectoryName(fileNames[i]);
        item.FullName = name;
        References.Add(item);
    }
}

La prima cosa che fa il metodo da noi usato, è controllare la cartella di output del progetto e leggerne tutti i file contenuti andando a inserirli nella lista dei file, escludendo i file relativi al debugger.

this.xmlDocument = new XmlDocument();
this.xmlDocument.Load(CsProjFileName);
this.xmlNamespaceManager = new XmlNamespaceManager(this.xmlDocument.NameTable);
this.xmlNamespaceManager.AddNamespace(XmlNameSpacePrefix, XmlNameSpace);
XmlNodeList list = xmlDocument.SelectNodes(AssemblyReferencesXPath, xmlNamespaceManager);
foreach (XmlNode node in list)
{
    foreach (XmlNode cnode in node.ChildNodes)
    {
        if (cnode.Name.Equals("HintPath", System.StringComparison.OrdinalIgnoreCase))
        {
            string name = Path.GetFileName(cnode.InnerText);
            if (References.ContainsName(name, "")) continue;
            ReferenceItem item = new ReferenceItem();
            item.Path = Path.GetDirectoryName(cnode.InnerText);
            item.FullName = name;
            References.Add(item);
        }
    }
}

La seconda cosa che fa questo metodo, è aprire il file di progetto e andare a cercare tutti i nodi di tipo HintPath.

    <Reference Include="EPPlus">
      <HintPath>B:\Dll3rdParty40\EPPlus\EPPlus.dll</HintPath>
    </Reference>
    <Reference Include="ITSB2B.Base.Settings.v4.0, Version=4.0.0.0, Culture=neutral, PublicKeyToken=6e5c2ba8e5e274d5, processorArchitecture=MSIL">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>B:\CommonDll40\ITSB2B.Base.Settings.v4.0.dll</HintPath>
    </Reference>
    <Reference Include="ITSB2B.Base.v4.0">
      <HintPath>B:\CommonDll40\ITSB2B.Base.v4.0.dll</HintPath>
    </Reference>

Se apriamo un file .csproj, troveremo che tutte le nostre dll referenziate, hanno questo formato. Pertanto InnoSetup può andare a leggere le librerie anche se sono pubblicate su una cartella diversa dalla cartella di progetto.

list = xmlDocument.SelectNodes(ContentReferencesXPath, xmlNamespaceManager);
foreach (XmlNode node in list)
{
    string filePath = node.Attributes["Include"].InnerText;
 
    bool isLink = false;
    foreach (XmlNode cnode in node.ChildNodes)
    {
        if (cnode.Name.Equals("link", System.StringComparison.OrdinalIgnoreCase))
        {
            isLink = true;
            break;
        }
    }
 
    ReferenceItem item = new ReferenceItem();
    string name = Path.GetFileName(filePath);
    string destpath = Path.GetDirectoryName(filePath).Replace(OutputRelativePath, "");
    if (References.ContainsName(name, destpath)) continue;
    if (!isLink)
    {
        item.DestinationPath = destpath;
 
        item.Path = Path.Combine(OutputRelativePath, Path.GetDirectoryName(filePath));
    }
    else
    {
        item.Path = Path.GetDirectoryName(filePath);
    }
    item.FullName = name;
    References.Add(item);
}

La terza ed ultima operazione, è quella che va a cercare tutti i file “ausiliari” che avessimo aggiunto all’applicazione e tutti i file collegati al progetto per aggiungerli alla lista dei file del setup.
I file ausiliari sono come già detto principalmente i file di contenuti che aggiungiamo ad un progetto, ad esempio file di testo (la licenza, il readme) file pdf o word (il manuale, l’help). Il fatto che il sistema cerchi anche quelli di tipologia “Link” è dovuto al fatto che in visual studio, è possibile aggiungere ad un progetto un file collegato, ovvero un file che appartiene ad un progetto ed è utilizzabile e condiviso con un diverso progetto. E’ una cosa che si fa raramente e che io sconsiglio, ma bisogna tenerne conto.

Tutto questo, ci permette di ottenere la lista dei file del nostro progetto generata automaticamente per produrre il setup.

Ovviamente questa utility è elementare, non va a cercare reference di tipo scalare leggendo i manifest delle DLL, non va neppure a cercare nel manifest dell’exe, pertanto non è precisa al 100% ma la uso da parecchi anni e avendo l’accortezza di ricordarsi di referenziare tutte le DLL e includere tutti i file necessari nell’exe funziona bene. Certamente è perfetta per i nostri piccoli progetti didattici come il Multiclock.exe.

Program.cs

internal class Program
{
...
}

E arriviamo ora alla classe che utilizza tutte quelle precedentemente definite:

private static void Main(string[] args)
{
    if (args == null || args.Length == 0 || args[0].Contains("?"))
    {
        ShowHelp();
    }
    else
    {
        GenerateFilesDependencies(args);
    }
}

Il Main dell’applicazione, che come possiamo vedere fa due cose, se l’utente digita un punto di domanda come argomento (o una stringa con un punto di domanda), o se non passa alcun argomento) viene visualizzato un help che mostra come si usa l’applicazione, esattamente quello che abbiamo inserito nell’immagine posta in “copertina” di questo articolo. Altrimenti procede a generare la lista dei file dell’applicazione.

private static void GenerateFilesDependencies(string[] args)
{
    try
    {
        StringBuilder sbWrong = new StringBuilder();
        Arguments arg = new Arguments();
 
        for (int i = 0; i < args.Length; i++)
        {
            //Project file 
            if (args[i].StartsWith("/p:"))
            {
                arg.CsProject = args[i].Substring(3);
            }
            //Output directory (Directory where files composing the project are created)
            else if(args[i].StartsWith("/d:"))
            {
                arg.OutputDirectory = args[i].Substring(3);
            }
            //Output file name (iss file)
            else if (args[i].StartsWith("/o:"))
            {
                arg.OutputFileName = args[i].Substring(3);
            }
            //Verbosity of the operation
            else if (args[i].StartsWith("/v:"))
            {
                Verbosity v = Verbosity.minimum;
                Enum.TryParse(args[i].Substring(3).ToLower(), out v);
                arg.Verbose = v;
            }
            else
            {
                sbWrong.AppendFormat(FMP_WrongParam, args[i]);
                sbWrong.AppendLine();
            }
        }
        if (sbWrong.Length > 0)
        {
            wr("PARAMETERS ERRORS");
            wr(sbWrong.ToString());
            Environment.ExitCode = 5;
            ShowHelp();
        }
        else
        {
            if (arg.OutputDirectory == null || arg.OutputDirectory.Trim().Length == 0
                || arg.CsProject == null || arg.CsProject.Trim().Length == 0
                || arg.OutputFileName == null || arg.OutputFileName.Trim().Length == 0)
            {
                wr("MISSING PARAMETERS");
                wr("One or more of the necessary parameters is missing!" );
                ShowHelp();
                Environment.ExitCode = 55;
                ShowHelp();
            }
            else
            {
                if (arg.Verbose != Verbosity.none)
                {
                    wr("Start processing");
                }
                string currentDir = Directory.GetCurrentDirectory();
                string destdir = Path.GetDirectoryName(arg.CsProject);
                Directory.SetCurrentDirectory(destdir);
                ReadFileReferences(arg);
                Directory.SetCurrentDirectory(currentDir);
            }
        }
    }
    catch (Exception ex)
    {
        wr("UNEXPECTED ERROR!");
        wr(ex.Message);
        Environment.ExitCode = 99;
    }
}

Il metodo GenerateFilesDependencies, per prima cosa processa tutti gli argomenti passati sulla linea di comando, estraendo i dati necessari all’elaborazione. Nel caso vi siano parametri errati o inaspettati da un errore ed esce dall’applicazione con il codice di errore 5.

Altrimenti, controlla se tutti i parametri indispensabili all’applicazione sono stati indicati, e in caso contrario, esce dall’elaborazione con codice di errore 55.

Se tutto quello che è necessario è stato indicato, cambia la cartella corrente sulla cartella del progetto, salvando il path corrente e chiama il metodo che va a comporre lo script con la lista dei file, al termine ripristina la cartella su cui si trovava in precedenza (questo è indispensabile perché se stiamo compilando una soluzione con più progetti o che genera più eseguibili, potrebbe bloccarsi con errore.

private static void ReadFileReferences(Arguments commandLineArgs)
{
    try
    {
        if (!File.Exists(commandLineArgs.CsProject))
        {
            wr("Execution error");
            wr("Selected project file does not exist!");
            wr("Creation terminated!");
            Environment.ExitCode = 9;
            return;
        }
 
        CsProjParser gen = new CsProjParser(commandLineArgs.OutputDirectory, commandLineArgs.CsProject);
        gen.GenerateReferences();
        string str = gen.References.ToInnoSetupString();
        File.WriteAllText(commandLineArgs.OutputFileName, str);
        if (commandLineArgs.Verbose == Verbosity.all)
        {
            wr(str);
        }
        if (commandLineArgs.Verbose != Verbosity.none)
        {
            wr("File {0} generated successfully", commandLineArgs.OutputFileName);
        }
    }
    catch (Exception ex)
    {
        wr("Unexpected Error!");
        wr(ex.Message);
        Environment.ExitCode = 88;
    }
}

Il metodo ReadFileReferences, controlla che il file di progetto esista ed in caso contrario da errore uscendo dall’applicazione con codice 9, genera il parser per i dati di progetto, esegue il metodo che genera la lista dei file e poi effettua l’output dei dati sul file .ISS da noi indicato. In caso di errore imprevisto, il sistema esce con codice di errore 88.

Il metodo ShowHelp ed il metodo Wr sono due metodi helper, che servono per la gestione degli errori e dell’help all’utente non credo serva descriverli.

Riepilogo

Creando questa piccola applicazione cosa abbiamo spiegato:

  • Come fare delle classi dati che non interagiscono con la User interface, quindi possono essere molto semplici.
  • Come creare un metodo che generi una stringa formattata a partire dai dati di una classe
  • Come si leggono i parametri da linea di comando in una applicazione console
  • Come si verificano e si trasformano in dati utilizzabili tali parametri da linea di comando.
  • Come si generano i codici di errore in uscita da un applicazione che possono essere intercettati usando un batch file.
  • Come si legge il contenuto di un file XML in modo non strutturato ma sequenziale, estraendo i valori di alcuni dei suoi nodi.

Nel prossimo articolo su Multiclock vedremo come utilizzarlo.

Potete scaricare il progetto esempio dal link qui indicato:

Se volete invece installare l’utility, questo è il link del setup

Per installare l’utility sul vostro PC basta unzippare il file eseguibile e porlo su una cartella da cui lo chiamerete in Visual Studio, solitamente per i progetti Dotnetwork, io creo una cartella C:\Dotnetwork dove metto le librerie e le utility. Il progetto, quando compilato da visual studio, copia automaticamente il file su quella cartella, generandola se non esiste.

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.