Dans cet article, on va enfin voir ensemble les dernières étapes de la génération du projet C, qui, pour rappel, est ce qui se cache derrière la sous-commande bsc :

bsc create PROJECT_NAME

 

Comme ceci est la suite directe de l’article précédent, je vous invite à lire Premiers pas en Rust : Partie 2 – Première commande de bsc et str vs String avant de débuter la lecture de celui-ci.

Vous êtes à jour ?

Alors on est reparti !

 

 

Que reste-il à faire ?

 

Si vous vous souvenez, dans l’article précédent, nous nous étions arrêtés à la création des dossiers src (contenant le code source des fichiers .c), test (qui lui contient le code source des fichiers .c pour les tests fonctionnels), include (pour les fichiers headers .h), et bsc_modules (pour stocker les modules dont le projet dépendra).

La tâche la plus importante restante est désormais de créer les fichiers dont on a besoin.

Pour rappel, il s’agit des fichiers :

  • CMakeLists.txt : un pour le projet dans sa globalité, et deux autres à placer dans src pour gérer la configuration « standard » permettant de compiler le programme; et dans test pour gérer la configuration de tests fonctionnels (je reviendrais sur ce principe ultérieurement, s’il est encore flou pour vous, ne vous en faites pas)
  • main.c : le fichier source à placer dans le dossier src qui contiendra un code « basique » en C (Hello World), et la fonction main d’entrée du programme
  • test.c : le fichier source de la configuration de tests à placer dans le dossier test, qui contiendra aussi du code « basique » en C, et la fonction main d’entrée du programme pour les tests fonctionnels
  • dependencies.bsc : le fichier qui gérera les modules (leurs version, liens de téléchargements…) dont dépendra le projet créé

Ah et dernière petite chose importante : pour vraiment terminer à 100% la création d’un projet, il faudra aussi initialiser un dépôt git (pourquoi pas ? Cargo le fait par exemple).

 

Factorisation du code : une fonction pour créer un fichier

 

Comme dans l’article précédent avec la création de dossier, on s’aperçoit vite que la création de fichier, la modification de son contenu, sa suppression… sont des fonctionnalités nécessaires au fonctionnement de bsc, ce qui implique qu’on risque de les utiliser assez souvent.

Cela vaut donc le coup de factoriser ces opérations dans des fonctions dédiées !

Ni d’une ni deux, on se retrouve donc dans dans le fichier common.rs, qui, pour rappel, contient tout le code réutilisé un peu partout dans notre programme.

Puisqu’on va utiliser des fonctions de la librairie standard relatives à la gestion de fichiers, j’ajoute l’instruction suivante en haut de common.rs :

// fs pour File System 
use std::fs;

 

et on commence par la première fonction de gestion de fichier : create_file :

pub fn create_file(file_path: &str, file_name: &str, file_content: &str){

    let mut file = match fs::File::create(format!("{}{}", &file_path, &file_name)) {
        // Si une erreur survient, j'arrête le programme et j'affiche un message et la raison de l'erreur (why.description())
        Err(why) => panic!("Error: couldn't create the {} file. {}", format!("{}{}", &file_path, &file_name), why.description()),
        // Si tout se passe bien, la variable file contient désormais une structure de type File de la librairie standard
        Ok(file) => file,
    };

    // J'écris le contenu de la chaîne de caractère file_content, passée en paramètre
    // dans le fichier, en gérant une erreur éventuelle d'écriture
    match file.write_all(file_content.as_bytes()) {
        Err(why) => panic!("Error: couldn't write to {}. {}", format!("{}{}", &file_path, &file_name), why.description()),
        Ok(_) => (),
    }
}

 

Elle est en fait très simple à comprendre.

Je commence par créer un fichier grâce à la fonction fs::File::create de la librairie standard. J’utilise la syntaxe propre à Rust pour faire un match directement à la déclaration de la variable file (qui est mutable, donc modifiable). Dans le cas où une erreur interviendrait lors de cette opération, j’affiche un message d’erreur grâce à la macro panic!, qui, pour rappel, à la même fonctionnalité que assert en C/C++ : cela quitte instantanément l’exécution du programme.

Si tout s’est bien passé, alors j’obtiens une variable de la structure file qui contient un fichier. Je peux maintenant écrire dedans avec la fonction write_all de cette structure.

Bien entendu, puisque cette fonction retourne (comme la plupart des fonctions en Rust) un Result, je dois m’occuper de catch une erreur éventuelle, d’où l’utilisation du match, pour effectuer plusieurs instructions en fonction du Result retourné par write_all.

Autre fait important, vous aurez sans doute remarqué l’appelle à as_bytes() lors de l’écriture de la chaîne de caractères file_content passée en paramètre de cette fonction :

match file.write_all(file_content.as_bytes()) { // sur cette ligne-ci
    Err(why) => panic!("Error: couldn't write to {}. {}", format!("{}{}", &file_path, &file_name), why.description()),
    Ok(_) => (),
}

 

Cette fonction propre à la structure str (également disponible pour un String), permet de convertir la chaîne de caractère UTF-8 contenue dans un str en buffer d’octets (un tableau d’octets, soit un [u8] en Rust). C’est une étape nécessaire puisque la fonction write_all permet d’écrire un buffer entier (jusqu’à ce qu’il n’y ait plus de data dans le buffer), et prend donc en paramètre un buffer d’octets, et non un str.

 

Création des fichiers sources (main.c et test.c)

 

Parfait ! Maintenant qu’on a notre fonction pour créer un fichier, et le remplir de contenu, on va alors pouvoir voir la génération des fichiers sources C (main.c et test.c).

Comme la génération de ces deux fichiers est une fonctionnalité propre à la création d’un projet, je créé une fonction create_main_file dans mon fichier create.rs :

pub fn create_main_file(path: &str, file_name: &str){

    // chaîne de caractère contenant le strict minimum pour compiler un projet en C
    let main_file_content = 
        "#include <stdio.h> \
        \n\nint main(int argc, char* argv[]){ \
        \n\tprintf(\"Hello World !\"); \
        \n\treturn 0; \
        \n}";

    common::create_file(&path, &file_name, &main_file_content);

}

 

Rien de bien compliqué ici au final, maintenant qu’on a notre fonction create_file dans le fichier common.rs.

Je créé simplement une chaîne de caractère contenant le minimum vital pour afficher un « Hello World ! » en C, en passant ensuite le chemin global (path, « ./src/ » dans mon cas) et le nom du fichier source, que je donne ensuite en paramètre de la fonction create_file précédemment créée.

Au passage, pour ceux qui se poseraient la question, les \n permettent de sauter une ligne (retour chariot en bon programmeur français), et les \t quant à eux, effectuent une tabulation.

Le fichier créé une fois la fonction appelée contiendra donc :

#include <stdio.h> 

int main(int argc, char* argv[]){ 
    printf("Hello World !"); 
    return 0; 
}

 

Bref, c’est tout simple, il n’y a plus qu’à l’appeler deux fois (une pour main.c et une pour test.c) dans la fonction create_project (qui est pour rappel le point d’entrée de la sub-command) :

pub fn create_project(path: &str, project_name: &str){

    println!("Project name: {}", &project_name);
    common::create_folder(&format!("{}{}", &path, "src"));
    common::create_folder(&format!("{}{}", &path, "test"));
    common::create_folder(&format!("{}{}", &path, "include"));
    common::create_folder("bsc_modules");

    // Création des deux fichiers sources C : main.c dans ./src/ et test.c dans ./test/
    create_main_file(&format!("{}{}", &path, "src/"), &"main.c");
    create_main_file(&format!("{}{}", &path, "test/"), &"test.c");

    println!("The project is correclty created.");
}

 

Niquel ! On a maintenant la moitié du travail de fait pour la sous-commande create.

Il ne nous reste plus qu’à créer les fichiers CMakeLists.txt pour gérer le build du projet C avec cmake, la création du fichier dependencies.bsc, et l’initialisation d’un dépôt git.

 

Création des fichiers CMakeLists.txt (configuration cmake)

 

On va devoir créer au total trois fichiers pour configurer le méta build cmake : un pour la configuration générale (qui s’occupe par exemple d’inclure les fichiers headers du dossier include dans nos deux configurations), et deux autres pour les configurations src et test. Ces deux configurations utiliseront les même headers, mais n’utiliseront pas le même code source. Ce qui donnera donc un exécutable différent à la fin de la compilation du projet.

 

Si vous n’avez aucune connaissance de cmake et son utilisation, ne vous en faites pas, je reviendrai dessus rapidement.
Vous allez voir, dans tous les cas il n’y a rien de compliqué ! 
🙂

 

Commençons par générer le fichier CMakeLists.txt global, c’est à dire celui gérant l’ensemble de notre projet C, constitué de nos deux configurations : src et test.

Pour cela, je créé comme précédemment une fonction dédiée à gérer cette fonctionnalité dans create.rs que je nomme tout simplement create_main_cmakelists_file :

// Cette fonction prend en paramètre le path du projet 
// (comme nous sommes constamment placé dans le dossier courant, cela sera toujours "./")
// Mais dans une éventuelle évolution de bsc, nous pourrons peut-être générer ce fichier depuis un autre dossier...

pub fn create_main_cmakelists_file(path: &str){

    let cmakelists_content = 
        "cmake_minimum_required (VERSION 3.9) \
        \n\n## Include libraries ## \
        \n\tinclude_directories (\"${PROJECT_BINARY_DIR}/../include\") \
        \n## End of include libraries ## \
        \n\nset(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}) \
        \n\nif (MSVC) \
        \n\tset(EXECUTABLE_OUTPUT_PATH bin/) \
        \nendif (MSVC) \
        \n\n## Add executables ## \
        \n\tadd_subdirectory (src) \
        \n\tadd_subdirectory (test) \
        \n## End of adding executables ##";

    common::create_file(&path, "CMakeLists.txt", &cmakelists_content);

}

 

Au final, rien de bien compliqué, c’est juste du texte simple qu’on va placer dans un fichier nommé CMakeLists.txt à la racine de notre projet.

Une fois qu’on ajoute cela dans notre fonction create_project de create.rs on a donc dorénavant à la fois la génération des dossiers, des fichiers sources, et du fichier CMakeLists.txt global :

pub fn create_project(path: &str, project_name: &str){

    println!("Project name: {}", &project_name);
    common::create_folder(&format!("{}{}", &path, "src".to_string()));
    common::create_folder(&format!("{}{}", &path, "test".to_string()));
    common::create_folder(&format!("{}{}", &path, "include".to_string()));
    common::create_folder("bsc_modules");
    create_main_file(&format!("{}{}", &path, "src/"), &"main.c");
    create_main_file(&format!("{}{}", &path, "test/"), &"test.c");

    // Génération du fichier CMakeLists.txt global
    create_main_cmakelists_file(path);

    println!("The project is correclty created.");

}

 

Voyons maintenant ensemble plus en détails la syntaxe cmake de notre fichier CMakeLists.txt global (voici ce qu’il contient) :

cmake_minimum_required (VERSION 3.9) 

## Include libraries ## 
  include_directories ("${PROJECT_BINARY_DIR}/../include") 
## End of include libraries ## 

set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}) 

if (MSVC) 
  set(EXECUTABLE_OUTPUT_PATH bin/) 
endif (MSVC) 

## Add executables ## 
  add_subdirectory (src) 
  add_subdirectory (test) 
## End of adding executables ##

 

Et voici ce que cela signifie, pour chaque ligne (les lignes commençant par le symbole # étant des commentaires) :

    • cmake_minimum_required => assez explicit, il faut posséder au minimum la version 3.9 de cmake pour pouvoir build le projet.
    • include_directories (« ${PROJECT_BINARY_DIR}/../include ») : inclut les fichiers headers présents dans le dossier include du projet. Le chemin du dossier include est donné par rapport aux fichiers générés par cmake. Habituellement, pour build un projet, on lance les commandes cmake depuis un dossier créé à la racine du projet (nommé par exemple build). Donc si on est situé dans un dossier build à la racine du projet, il faut revenir au dossier parent pour accéder ensuite à include, d’où le « /../ » tout simplement.
    • set (EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}) : pour placer les exécutables générés lors de la compilation dans un dossier portant le nom de la configuration de build (Debug ou Release), qu’on spécifie en appelant la commande cmake. Ce dossier est lui-même placé dans un dossier nommé « bin« . Cela évite d’obtenir plein de fichiers générés au même endroit, pour structurer un peu.
    • if (MSVC)
          set(EXECUTABLE_OUTPUT_PATH bin/)
      endif (MSVC)

      Si la compilation se fait via le compilateur de Visual Studio (sur PC Windows), alors on place les exécutables dans le dossier « bin ». C’est la même chose que la ligne ci-dessus, cependant, pour une raison obscure, le compilateur de Visual Studio créé automatiquement un dossier pour les configurations de Debug et de Release, donc il n’y a pas besoin de l’ajouter comme ci-dessus, sinon, cela ferait doublon (on se retrouverait avec les exécutables dans le dossier bin/Debug/Debug/ par exemple).
  • add_subdirectory (src)
    add_subdirectory (test)

A partir de là, cmake va continuer d’effectuer les instructions qui sont situées dans les fichiers CMakeLists.txt des dossiers src et test. 

 

Voyons maintenant la création des fichiers CMakeLists.txt dans les dossiers src et test.

Pour gérer la création de ces fichiers spécifiques, je créé une nouvelle fonction create_secondary_cmakelists_files dans le fichier create.rs :

pub fn create_secondary_cmakelists_file(path: &str, project_name: &str){

    let final_project_name = project_name.to_string();
    let mut secondary_cmakelists_content = format!(
        "project({}) \
        \n\nset(EXECUTABLE_OUTPUT_PATH bin/${{CMAKE_BUILD_TYPE}}) \
        \n\n## Add source files ## \
        \n\tfile (GLOB_RECURSE source_files ./*) \
        \n## End of adding source files ## \
        \n\n## Remove main.c files of modules ## \
        \n## End of removing main.c files of modules ## \
        \n\n## Add executables ## \
        \n\tadd_executable ({} ${{source_files}}) \
        \n## End of adding executables ##", 
        final_project_name, final_project_name);

    // Dans le cas où je créé le fichier pour le programme de test, alors
    // je rajoute les lignes ci-dessous qui me permettent d'enlever
    // le fichier src/main.c des fichiers sources, pour n'avoir
    // qu'une seule fonction d'entrée du programme int main() 
    // qui est présente dans test/test.c
    if path.to_string().contains("test"){
        secondary_cmakelists_content = format!(
            "project(test_{}) \
            \n\nset(EXECUTABLE_OUTPUT_PATH bin/${{CMAKE_BUILD_TYPE}}) \
            \n\n## Add source files ## \
            \n\tfile (GLOB_RECURSE testing_files ./*) \
            \n\tfile (GLOB_RECURSE testing_source_files ../src/*) \
            \n## End of adding source files ## \
            \n\n## Remove main.c files of modules ## \
            \n\tFOREACH(item ${{testing_source_files}}) \
            \n\t\tIF(${{item}} MATCHES \"main.c\") \
            \n\t\t\tLIST(REMOVE_ITEM testing_source_files ${{item}}) \
            \n\t\tENDIF(${{item}} MATCHES \"main.c\") \
            \n\tENDFOREACH(item) \
            \n## End of removing main.c files of modules ## \
            \n\n## Add executables ## \
            \n\tadd_executable (test_{} ${{testing_files}} ${{testing_source_files}}) \
            \n## End of adding executables ##", final_project_name, final_project_name);
    }

    common::create_file(&path, "CMakeLists.txt", &secondary_cmakelists_content);

}

 

Oula, ça fait pas mal de lignes de code tout ça !

 

Effectivement, mais rassurez-vous, encore une fois, il n’y a rien de bien compliqué.

Tout ce que je fais dans cette fonction, c’est créer une chaîne de caractère, en concaténant plusieurs chaînes à l’aide de la macro format! que vous avez sans doute reconnu.

Puis, je passe la chaîne générée (nommée secondary_cmakelists_content) à la fonction create_file pour générer le fichier.

Je rajoute ensuite deux lignes (une pour src et une pour test) dans ma fonction d’entrée create_project du même fichier, comme d’habitude :

pub fn create_project(path: &str, project_name: &str){

    println!("Project name: {}", &project_name);
    common::create_folder(&format!("{}{}", &path, "src"));
    common::create_folder(&format!("{}{}", &path, "test"));
    common::create_folder(&format!("{}{}", &path, "include"));
    common::create_folder("bsc_modules");
    create_main_file(&format!("{}{}", &path, "src/"), &"main.c");
    create_main_file(&format!("{}{}", &path, "test/"), &"test.c");
    create_main_cmakelists_file(path);

    // Génération des fichiers CMakeLists.txt dans les dossiers src et test
    create_secondary_cmakelists_file(&format!("{}{}", &path, "src/"), &project_name);
    create_secondary_cmakelists_file(&format!("{}{}", &path, "test/"), &project_name);
    println!("The project is correclty created.");

}

 

Voyons maintenant, plus en détails le contenu de ces deux fichiers, en commençant par celui présent dans src :

project(test_bsc) 


## Add source files ## 
  file (GLOB_RECURSE source_files ./*) 
## End of adding source files ## 

## Add executables ## 
  add_executable (test_bsc ${source_files}) 
## End of adding executables ##

 

Et encore une fois, voici l’explication ligne par ligne :

  • project(test_bsc) : ici, je précise le nom du projet, c’est à dire le nom de l’exécutable qui sera généré lors de la compilation. En fait, il s’agit ni plus ni moins que l’argument qu’on a passé dans la sous-commande (bsc create PROJECT_NAME).

 

  • file (GLOB_RECURSE source_files ./*) : pour spécifier où trouver les fichiers sources (.c). Dans notre cas, tous nos fichiers sources sont dans le dossier src, c’est à dire le dossier courant (./), par rapport à ce fichier CMakeLists.txt qui se trouve également dans src.

 

  • add_executable (test_bsc ${source_files}) : enfin, avec cette ligne, cmake comprend qu’on doit générer un exécutable pour le projet nommé test_bsc et que celui-ci doit inclure les fichiers sources définis juste au dessus.

 

Et vous allez sans doute me dire qu’il manque les fichiers headers pour compiler ce projet, non ?

Mais souvenez-vous, on a justement généré un fichier CMakeLists.txt « global » précédemment pour les inclure, donc il n’y a pas besoin de le repréciser ici.

 

Regardons maintenant ce qu’il en est concernant l’autre fichier CMakeLists.txt présent dans le dossier test cette fois :

project(test_test_bsc) 

## Add source files ## 
  file (GLOB_RECURSE testing_files ./*) 
  file (GLOB_RECURSE testing_source_files ../src/*) 
## End of adding source files ## 

## Remove main.c files of modules ## 
  FOREACH(item ${testing_source_files}) 
    IF(${item} MATCHES "main.c") 
      LIST(REMOVE_ITEM testing_source_files ${item}) 
    ENDIF(${item} MATCHES "main.c") 
  ENDFOREACH(item) 
## End of removing main.c files of modules ## 

## Add executables ## 
  add_executable (test_test_bsc ${testing_files} ${testing_source_files}) 
## End of adding executables ##

 

Il est un peu plus rempli, mais vous allez voir, c’est tout bête !

    • project(test_test_bsc) : c’est la même chose que pour le fichier précédent, mais je rajoute à chaque fois le préfixe « test_ » accolé au nom du projet. En gros, ça fait qu’à chaque compilation, on obtiendra toujours un exécutable pour la configuration de test nommé test_nom_du_projet). Ici, comme notre projet s’appelle test_bsc, on va donc générer un programme de test nommé test_test_bsc (oui, c’est un peu redondant pour ce cas précis haha).

 

    • file (GLOB_RECURSE testing_files ./*) : Comme pour le fichier précédent, encore une fois, on inclut grâce à cette ligne les fichiers sources spécifiques à la configuration de test, présents dans le dossier test (c’est à dire le dossier courant « ./ » par rapport à ce fichier CMakeLists.txt).

 

    • file (GLOB_RECURSE testing_source_files ../src/*) : En plus d’avoir besoin des fichiers sources propres à la configuration de test, on a aussi besoin des fichiers sources « globaux » du projet, et ces derniers sont situés dans le dossier src, (« ../src » par rapport à notre dossier courant). Je désigne ici une variable testing_source_files qui contient ces fichiers.

 

    • FOREACH(item ${testing_source_files})
          IF(${item} MATCHES « main.c »)
      LIST(REMOVE_ITEM testing_source_files ${item})
      ENDIF(${item} MATCHES « main.c »)
      ENDFOREACH(item)

Ce bloc paraît imposant, mais en fait, encore une fois, il est tout simple. Comme évoqué précédemment, notre exécutable généré en configuration de test a besoin des fichiers sources présents dans le dossier src. Cependant, ces fichiers sources incluent le fichier main.c, qui contient, comme test.c (présent dans le dossier test), une fonction d’entrée du programme int main(…).Or il ne peut y avoir qu’une seule fonction d’entrée par programme. Ce bloc permet donc, tout simplement, d’enlever le fichier main.c des fichiers sources pour générer l’exécutable de la configuration de test.

 

Si vous ne comprenez pas encore vraiment comment cela fonctionne, et pourquoi j’ai choisi ce système de configuration de test, ne vous en faites pas.
Un article constitué uniquement d’exemples d’utilisation de la sous-commande bsc create arrive très prochainement pour vous expliquer tout cela plus en détails. 🙂

 

  • add_executable (test_test_bsc ${testing_files} ${testing_source_files}) : Enfin, comme précédemment, j’inclus ici les fichiers sources présents dans le dossier courant (test) grâce à la variable testing_files, et les fichiers sources du dossier src (testing_source_files) pour compiler ce projet test_test_bsc.

 

Et voilà, la génération du squelette du projet C est quasiment terminée. Il ne reste plus qu’à initialiser le dépôt git, et à créer le fichier dependencies.bsc permettant de gérer, par la suite, les modules dont le projet va dépendre.

Comme je m’aperçois que cet article contient déjà beaucoup d’informations (et donc, que cela fait probablement beaucoup trop de lignes à lire 😉 ),  j’expliquerai ces dernières étapes dans la partie 4 de cette série Premiers pas en Rust.

Dans tous les cas, encore une fois, je vous remercie d’avoir lu l’article jusqu’au bout, en espérant que vous l’avez trouvé intéressant, et/ou que vous avez appris des choses.

Comme d’habitude, je reste à l’écoute d’éventuelles remarques ou commentaires, que ce soit sur le fond, comme sur la forme. Vous pouvez me communiquer vos idées / remarques / objections, via les commentaires en dessous de cet article.

 

 

Merci d’avoir pris le temps de lire cet article. Est-ce que cela vous a intéressé ? Est-ce que vous avez appris quelque chose ? N’hésitez pas à me partager votre opinion sur le sujet via les commentaires ci-dessous.

Et si vous aimez ce contenu, vous pouvez vous abonnez à ma newsletter (si le coeur vous en dit), afin de rester informé des dernières nouveautés du blog, et également pour recevoir (pas plus d’une fois par semaine), d’autres contenus que je juge intéressant sur tout ce qui touche la programmation ou le développement personnel.

Inscription à la newsletter :

[newsletter]

 

Victor Gallet

Victor Gallet

Étudiant programmeur jeu vidéo. J'aime par dessus tout apprendre, et je suis un éternel curieux de tout. Mon principal but dans la vie est d’être une meilleure personne, et de partager mes (faibles) connaissances avec les autres.

Leave a Reply

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.