use module names, not file names, in menuselect
[asterisk/asterisk.git] / build_tools / menuselect.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2005 - 2006, Russell Bryant
5  *
6  * Russell Bryant <russell@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*
20  * \file
21  *
22  * \author Russell Bryant <russell@digium.com>
23  * 
24  * \brief A menu-driven system for Asterisk module selection
25  */
26
27 #include "autoconfig.h"
28
29 #include <stdlib.h>
30 #include <stdio.h>
31 #include <string.h>
32 #include <unistd.h>
33
34 #include "mxml/mxml.h"
35 #include "menuselect.h"
36
37 #include "asterisk.h"
38
39 #include "asterisk/linkedlists.h"
40
41 #undef MENUSELECT_DEBUG
42
43 /*! The list of categories */
44 struct categories categories = AST_LIST_HEAD_NOLOCK_INIT_VALUE;
45
46 /*!
47    We have to maintain a pointer to the root of the trees generated from reading
48    the build options XML files so that we can free it when we're done.  We don't
49    copy any of the information over from these trees. Our list is just a 
50    convenient mapping to the information contained in these lists with one
51    additional piece of information - whether the build option is enabled or not.
52 */
53 struct tree {
54         /*! the root of the tree */
55         mxml_node_t *root;
56         /*! for linking */
57         AST_LIST_ENTRY(tree) list;
58 };
59
60 /*! The list of trees from makeopts.xml files */
61 static AST_LIST_HEAD_NOLOCK_STATIC(trees, tree);
62
63 static const char * const makeopts_files[] = {
64         "makeopts.xml"
65 };
66
67 static char *output_makeopts = OUTPUT_MAKEOPTS_DEFAULT;
68
69 /*! This is set to 1 if menuselect.makeopts pre-existed the execution of this app */
70 static int existing_config = 0;
71
72 /*! This is set when the --check-deps argument is provided. */
73 static int check_deps = 0;
74
75 /*! Force a clean of the source tree */
76 static int force_clean = 0;
77
78 static int add_category(struct category *cat);
79 static int add_member(struct member *mem, struct category *cat);
80 static int parse_makeopts_xml(const char *makeopts_xml);
81 static int process_deps(void);
82 static int build_member_list(void);
83 static void mark_as_present(const char *member, const char *category);
84 static void process_prev_failed_deps(char *buf);
85 static int parse_existing_config(const char *infile);
86 static int generate_makeopts_file(void);
87 static void free_member_list(void);
88 static void free_trees(void);
89
90 /*! \brief return a pointer to the first non-whitespace character */
91 static inline char *skip_blanks(char *str)
92 {
93         if (!str)
94                 return NULL;
95
96         while (*str && *str < 33)
97                 str++;
98
99         return str;
100 }
101
102 /*! \brief Add a category to the category list, ensuring that there are no duplicates */
103 static int add_category(struct category *cat)
104 {
105         struct category *tmp;
106
107         AST_LIST_TRAVERSE(&categories, tmp, list) {
108                 if (!strcmp(tmp->name, cat->name)) {
109                         fprintf(stderr, "Category '%s' specified more than once!\n", cat->name);
110                         return -1;
111                 }
112         }
113         AST_LIST_INSERT_TAIL(&categories, cat, list);
114
115         return 0;
116 }
117
118 /*! \brief Add a member to the member list of a category, ensuring that there are no duplicates */
119 static int add_member(struct member *mem, struct category *cat)
120 {
121         struct member *tmp;
122
123         AST_LIST_TRAVERSE(&cat->members, tmp, list) {
124                 if (!strcmp(tmp->name, mem->name)) {
125                         fprintf(stderr, "Member '%s' already exists in category '%s', ignoring.\n", mem->name, cat->name);
126                         return -1;
127                 }
128         }
129         AST_LIST_INSERT_TAIL(&cat->members, mem, list);
130
131         return 0;
132 }
133
134 /*! \brief Parse an input makeopts file */
135 static int parse_makeopts_xml(const char *makeopts_xml)
136 {
137         FILE *f;
138         struct category *cat;
139         struct tree *tree;
140         struct member *mem;
141         struct depend *dep;
142         struct conflict *cnf;
143         mxml_node_t *cur;
144         mxml_node_t *cur2;
145         mxml_node_t *cur3;
146         mxml_node_t *menu;
147         const char *tmp;
148
149         if (!(f = fopen(makeopts_xml, "r"))) {
150                 fprintf(stderr, "Unable to open '%s' for reading!\n", makeopts_xml);
151                 return -1;
152         }
153
154         if (!(tree = calloc(1, sizeof(*tree)))) {
155                 fclose(f);
156                 return -1;
157         }
158
159         if (!(tree->root = mxmlLoadFile(NULL, f, MXML_OPAQUE_CALLBACK))) {
160                 fclose(f);
161                 free(tree);
162                 return -1;
163         }
164
165         AST_LIST_INSERT_HEAD(&trees, tree, list);
166
167         menu = mxmlFindElement(tree->root, tree->root, "menu", NULL, NULL, MXML_DESCEND);
168         for (cur = mxmlFindElement(menu, menu, "category", NULL, NULL, MXML_DESCEND);
169              cur;
170              cur = mxmlFindElement(cur, menu, "category", NULL, NULL, MXML_DESCEND))
171         {
172                 if (!(cat = calloc(1, sizeof(*cat))))
173                         return -1;
174
175                 cat->name = mxmlElementGetAttr(cur, "name");
176                 cat->displayname = mxmlElementGetAttr(cur, "displayname");
177                 if ((tmp = mxmlElementGetAttr(cur, "positive_output")))
178                         cat->positive_output = !strcasecmp(tmp, "yes");
179                 if ((tmp = mxmlElementGetAttr(cur, "force_clean_on_change")))
180                         cat->force_clean_on_change = !strcasecmp(tmp, "yes");
181
182                 if (add_category(cat)) {
183                         free(cat);
184                         continue;
185                 }
186
187                 for (cur2 = mxmlFindElement(cur, cur, "member", NULL, NULL, MXML_DESCEND);
188                      cur2;
189                      cur2 = mxmlFindElement(cur2, cur, "member", NULL, NULL, MXML_DESCEND))
190                 {
191                         if (!(mem = calloc(1, sizeof(*mem))))
192                                 return -1;
193                         
194                         mem->name = mxmlElementGetAttr(cur2, "name");
195                         mem->displayname = mxmlElementGetAttr(cur2, "displayname");
196                 
197                         if (!cat->positive_output)
198                                 mem->enabled = 1;
199         
200                         cur3 = mxmlFindElement(cur2, cur2, "defaultenabled", NULL, NULL, MXML_DESCEND);
201                         if (cur3 && cur3->child)
202                                 mem->defaultenabled = cur3->child->value.opaque;
203                         
204                         for (cur3 = mxmlFindElement(cur2, cur2, "depend", NULL, NULL, MXML_DESCEND);
205                              cur3 && cur3->child;
206                              cur3 = mxmlFindElement(cur3, cur2, "depend", NULL, NULL, MXML_DESCEND))
207                         {
208                                 if (!(dep = calloc(1, sizeof(*dep))))
209                                         return -1;
210                                 if (!strlen_zero(cur3->child->value.opaque)) {
211                                         dep->name = cur3->child->value.opaque;
212                                         AST_LIST_INSERT_HEAD(&mem->deps, dep, list);
213                                 } else
214                                         free(dep);
215                         }
216
217                         for (cur3 = mxmlFindElement(cur2, cur2, "conflict", NULL, NULL, MXML_DESCEND);
218                              cur3 && cur3->child;
219                              cur3 = mxmlFindElement(cur3, cur2, "conflict", NULL, NULL, MXML_DESCEND))
220                         {
221                                 if (!(cnf = calloc(1, sizeof(*cnf))))
222                                         return -1;
223                                 if (!strlen_zero(cur3->child->value.opaque)) {
224                                         cnf->name = cur3->child->value.opaque;
225                                         AST_LIST_INSERT_HEAD(&mem->conflicts, cnf, list);
226                                 } else
227                                         free(cnf);
228                         }
229
230                         if (add_member(mem, cat))
231                                 free(mem);
232                 }
233         }
234
235         fclose(f);
236
237         return 0;
238 }
239
240 /*! \brief Process dependencies against the input dependencies file */
241 static int process_deps(void)
242 {
243         struct category *cat;
244         struct member *mem;
245         struct depend *dep;
246         struct conflict *cnf;
247         FILE *f;
248         struct dep_file {
249                 char name[32];
250                 int met;
251                 AST_LIST_ENTRY(dep_file) list;
252         } *dep_file;
253         AST_LIST_HEAD_NOLOCK_STATIC(deps_file, dep_file);
254         char buf[80];
255         char *p;
256         int res = 0;
257
258         if (!(f = fopen(MENUSELECT_DEPS, "r"))) {
259                 fprintf(stderr, "Unable to open '%s' for reading!  Did you run ./configure ?\n", MENUSELECT_DEPS);
260                 return -1;
261         }
262
263         /* Build a dependency list from the file generated by configure */      
264         while (memset(buf, 0, sizeof(buf)), fgets(buf, sizeof(buf), f)) {
265                 p = buf;
266                 strsep(&p, "=");
267                 if (!p)
268                         continue;
269                 if (!(dep_file = calloc(1, sizeof(*dep_file))))
270                         break;
271                 strncpy(dep_file->name, buf, sizeof(dep_file->name) - 1);
272                 dep_file->met = atoi(p);
273                 AST_LIST_INSERT_TAIL(&deps_file, dep_file, list);
274         }
275
276         fclose(f);
277
278         /* Process dependencies of all modules */
279         AST_LIST_TRAVERSE(&categories, cat, list) {
280                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
281                         AST_LIST_TRAVERSE(&mem->deps, dep, list) {
282                                 mem->depsfailed = 1;
283                                 AST_LIST_TRAVERSE(&deps_file, dep_file, list) {
284                                         if (!strcasecmp(dep_file->name, dep->name)) {
285                                                 if (dep_file->met)
286                                                         mem->depsfailed = 0;
287                                                 break;
288                                         }
289                                 }
290                                 if (mem->depsfailed)
291                                         break; /* This dependency is not met, so we can stop now */
292                         }
293                 }
294         }
295
296         /* Process conflicts of all modules */
297         AST_LIST_TRAVERSE(&categories, cat, list) {
298                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
299                         AST_LIST_TRAVERSE(&mem->conflicts, cnf, list) {
300                                 mem->conflictsfailed = 0;
301                                 AST_LIST_TRAVERSE(&deps_file, dep_file, list) {
302                                         if (!strcasecmp(dep_file->name, cnf->name)) {
303                                                 if (dep_file->met)
304                                                         mem->conflictsfailed = 1;
305                                                 break;
306                                         }
307                                 }
308                                 if (mem->conflictsfailed)
309                                         break; /* This conflict was found, so we can stop now */
310                         }
311                 }
312         }
313
314         /* Free the dependency list we built from the file */
315         while ((dep_file = AST_LIST_REMOVE_HEAD(&deps_file, list)))
316                 free(dep_file);
317
318         return res;
319 }
320
321 /*! \brief Iterate through all of the input makeopts files and call the parse function on them */
322 static int build_member_list(void)
323 {
324         int i;
325         int res = -1;
326
327         for (i = 0; i < (sizeof(makeopts_files) / sizeof(makeopts_files[0])); i++) {
328                 if ((res = parse_makeopts_xml(makeopts_files[i]))) {
329                         fprintf(stderr, "Error parsing '%s'!\n", makeopts_files[i]);
330                         break;
331                 }
332         }
333
334         return res;
335 }
336
337 /*! \brief Given the string representation of a member and category, mark it as present in a given input file */
338 static void mark_as_present(const char *member, const char *category)
339 {
340         struct category *cat;
341         struct member *mem;
342
343         AST_LIST_TRAVERSE(&categories, cat, list) {
344                 if (strcmp(category, cat->name))
345                         continue;
346                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
347                         if (!strcmp(member, mem->name)) {
348                                 mem->enabled = cat->positive_output;
349                                 break;
350                         }
351                 }
352                 if (!mem)
353                         fprintf(stderr, "member '%s' in category '%s' not found, ignoring.\n", member, category);
354                 break;
355         }
356
357         if (!cat)
358                 fprintf(stderr, "category '%s' not found! Can't mark '%s' as disabled.\n", category, member);
359 }
360
361 /*! \brief Toggle a member of a category at the specified index to enabled/disabled */
362 void toggle_enabled(struct category *cat, int index)
363 {
364         struct member *mem;
365         int i = 0;
366
367         AST_LIST_TRAVERSE(&cat->members, mem, list) {
368                 if (i++ == index)
369                         break;
370         }
371
372         if (mem && !(mem->depsfailed || mem->conflictsfailed)) {
373                 mem->enabled = !mem->enabled;
374                 if (cat->force_clean_on_change)
375                         force_clean = 1;
376         }
377 }
378
379 /*! \brief Process a previously failed dependency
380  *
381  * If a module was previously disabled because of a failed dependency
382  * or a conflict, and not because the user selected it to be that way,
383  * then it needs to be re-enabled by default if the problem is no longer present.
384  */
385 static void process_prev_failed_deps(char *buf)
386 {
387         const char *cat_name, *mem_name;
388         struct category *cat;
389         struct member *mem;
390
391         cat_name = strsep(&buf, "=");
392         mem_name = strsep(&buf, "\n");
393
394         if (!cat_name || !mem_name)
395                 return;
396
397         AST_LIST_TRAVERSE(&categories, cat, list) {
398                 if (strcasecmp(cat->name, cat_name))
399                         continue;
400                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
401                         if (strcasecmp(mem->name, mem_name))
402                                 continue;
403
404                         if (!mem->depsfailed && !mem->conflictsfailed)
405                                 mem->enabled = 1;                       
406         
407                         break;
408                 }
409                 break;  
410         }
411
412         if (!cat || !mem)
413                 fprintf(stderr, "Unable to find '%s' in category '%s'\n", mem_name, cat_name);
414 }
415
416 /*! \brief Parse an existing output makeopts file and enable members previously selected */
417 static int parse_existing_config(const char *infile)
418 {
419         FILE *f;
420         char buf[2048];
421         char *category, *parse, *member;
422         int lineno = 0;
423
424         if (!(f = fopen(infile, "r"))) {
425 #ifdef MENUSELECT_DEBUG
426                 /* This isn't really an error, so only print the message in debug mode */
427                 fprintf(stderr, "Unable to open '%s' for reading existing config.\n", infile);
428 #endif  
429                 return -1;
430         }
431
432         while (fgets(buf, sizeof(buf), f)) {
433                 lineno++;
434
435                 if (strlen_zero(buf))
436                         continue;
437
438                 /* skip lines that are not for this tool */
439                 if (strncasecmp(buf, "MENUSELECT_", strlen("MENUSELECT_")))
440                         continue;
441
442                 parse = buf;
443                 parse = skip_blanks(parse);
444                 if (strlen_zero(parse))
445                         continue;
446
447                 /* Grab the category name */    
448                 category = strsep(&parse, "=");
449                 if (!parse) {
450                         fprintf(stderr, "Invalid string in '%s' at line '%d'!\n", output_makeopts, lineno);
451                         continue;
452                 }
453                 
454                 parse = skip_blanks(parse);
455         
456                 if (!strcasecmp(category, "MENUSELECT_DEPSFAILED")) {
457                         process_prev_failed_deps(parse);
458                         continue;
459                 }
460         
461                 while ((member = strsep(&parse, " \n"))) {
462                         member = skip_blanks(member);
463                         if (strlen_zero(member))
464                                 continue;
465                         mark_as_present(member, category);
466                 }
467         }
468
469         fclose(f);
470
471         return 0;
472 }
473
474 /*! \brief Create the output makeopts file that results from the user's selections */
475 static int generate_makeopts_file(void)
476 {
477         FILE *f;
478         struct category *cat;
479         struct member *mem;
480
481         if (!(f = fopen(output_makeopts, "w"))) {
482                 fprintf(stderr, "Unable to open build configuration file (%s) for writing!\n", output_makeopts);
483                 return -1;
484         }
485
486         /* Traverse all categories and members and output them as var/val pairs */
487         AST_LIST_TRAVERSE(&categories, cat, list) {
488                 fprintf(f, "%s=", cat->name);
489                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
490                         if ((!cat->positive_output && (!mem->enabled || mem->depsfailed || mem->conflictsfailed)) ||
491                             (cat->positive_output && mem->enabled && !mem->depsfailed && !mem->conflictsfailed))
492                                 fprintf(f, "%s ", mem->name);
493                 }
494                 fprintf(f, "\n");
495         }
496
497         /* Output which members were disabled because of failed dependencies or conflicts */
498         AST_LIST_TRAVERSE(&categories, cat, list) {
499                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
500                         if (mem->depsfailed || mem->conflictsfailed)
501                                 fprintf(f, "MENUSELECT_DEPSFAILED=%s=%s\n", cat->name, mem->name);
502                 }
503         }
504
505         fclose(f);
506
507         return 0;
508 }
509
510 #ifdef MENUSELECT_DEBUG
511 /*! \brief Print out all of the information contained in our tree */
512 static void dump_member_list(void)
513 {
514         struct category *cat;
515         struct member *mem;
516         struct depend *dep;
517         struct conflict *cnf;
518
519         AST_LIST_TRAVERSE(&categories, cat, list) {
520                 fprintf(stderr, "Category: '%s'\n", cat->name);
521                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
522                         fprintf(stderr, "   ==>> Member: '%s'  (%s)\n", mem->name, mem->enabled ? "Enabled" : "Disabled");
523                         AST_LIST_TRAVERSE(&mem->deps, dep, list)
524                                 fprintf(stderr, "      --> Depends on: '%s'\n", dep->name);
525                         if (!AST_LIST_EMPTY(&mem->deps))
526                                 fprintf(stderr, "      --> Dependencies Met: %s\n", mem->depsfailed ? "No" : "Yes");    
527                         AST_LIST_TRAVERSE(&mem->conflicts, cnf, list)
528                                 fprintf(stderr, "      --> Conflicts with: '%s'\n", cnf->name);
529                         if (!AST_LIST_EMPTY(&mem->conflicts))
530                                 fprintf(stderr, "      --> Conflicts Found: %s\n", mem->conflictsfailed ? "Yes" : "No");
531                 }
532         }
533 }
534 #endif
535
536 /*! \brief Free all categories and their members */
537 static void free_member_list(void)
538 {
539         struct category *cat;
540         struct member *mem;
541         struct depend *dep;
542         struct conflict *cnf;
543
544         while ((cat = AST_LIST_REMOVE_HEAD(&categories, list))) {
545                 while ((mem = AST_LIST_REMOVE_HEAD(&cat->members, list))) {
546                         while ((dep = AST_LIST_REMOVE_HEAD(&mem->deps, list)))
547                                 free(dep);
548                         while ((cnf = AST_LIST_REMOVE_HEAD(&mem->conflicts, list)))
549                                 free(cnf);
550                         free(mem);
551                 }
552                 free(cat);
553         }
554 }
555
556 /*! \brief Free all of the XML trees */
557 static void free_trees(void)
558 {
559         struct tree *tree;
560
561         while ((tree = AST_LIST_REMOVE_HEAD(&trees, list))) {
562                 mxmlDelete(tree->root);
563                 free(tree);
564         }
565 }
566
567 /*! \brief Enable/Disable all members of a category as long as dependencies have been met and no conflicts are found */
568 void set_all(struct category *cat, int val)
569 {
570         struct member *mem;
571
572         AST_LIST_TRAVERSE(&cat->members, mem, list) {
573                 if (!(mem->depsfailed || mem->conflictsfailed))
574                         mem->enabled = val;
575         }
576 }
577
578 int count_categories(void)
579 {
580         struct category *cat;
581         int count = 0;
582
583         AST_LIST_TRAVERSE(&categories, cat, list)
584                 count++;
585
586         return count;           
587 }
588
589 int count_members(struct category *cat)
590 {
591         struct member *mem;
592         int count = 0;
593
594         AST_LIST_TRAVERSE(&cat->members, mem, list)
595                 count++;
596
597         return count;           
598 }
599
600 /*! \brief Make sure an existing menuselect.makeopts disabled everything it should have */
601 static int sanity_check(void)
602 {
603         struct category *cat;
604         struct member *mem;
605
606         AST_LIST_TRAVERSE(&categories, cat, list) {
607                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
608                         if ((mem->depsfailed || mem->conflictsfailed) && mem->enabled) {
609                                 fprintf(stderr, "\n***********************************************************\n"
610                                                 "  The existing menuselect.makeopts file did not specify    \n"
611                                                 "  that '%s' should not be included.  However, either some  \n"
612                                                 "  dependencies for this module were not found or a         \n"
613                                                 "  conflict exists.                                         \n"
614                                                 "                                                           \n"
615                                                 "  Either run 'make menuselect' or remove the existing      \n"
616                                                 "  menuselect.makeopts file to resolve this issue.          \n"
617                                                 "***********************************************************\n\n", mem->name);
618                                 return -1;
619                         }
620                 }
621         }
622         return 0;       /* all good... */
623 }
624
625 /* \brief Set the forced default values if they exist */
626 static void process_defaults(void)
627 {
628         struct category *cat;
629         struct member *mem;
630
631         AST_LIST_TRAVERSE(&categories, cat, list) {
632                 AST_LIST_TRAVERSE(&cat->members, mem, list) {
633                         if (!mem->defaultenabled)
634                                 continue;
635                         
636                         if (!strcasecmp(mem->defaultenabled, "yes"))
637                                 mem->enabled = 1;
638                         else if (!strcasecmp(mem->defaultenabled, "no"))
639                                 mem->enabled = 0;
640                         else
641                                 fprintf(stderr, "Invalid defaultenabled value for '%s' in category '%s'\n", mem->name, cat->name);      
642                 }
643         }
644
645 }
646
647 int main(int argc, char *argv[])
648 {
649         int res = 0;
650         unsigned int x;
651
652         /* Parse the input XML files to build the list of available options */
653         if ((res = build_member_list()))
654                 exit(res);
655         
656         /* Process module dependencies */
657         res = process_deps();
658         
659         /* The --check-deps option is used to ask this application to check to
660          * see if that an existing menuselect.makeopts file contails all of the
661          * modules that have dependencies that have not been met.  If this
662          * is not the case, an informative message will be printed to the
663          * user and the build will fail. */
664         for (x = 1; x < argc; x++) {
665                 if (!strcmp(argv[x], "--check-deps"))
666                         check_deps = 1;
667                 else {
668                         res = parse_existing_config(argv[x]);
669                         if (!res && !strcasecmp(argv[x], OUTPUT_MAKEOPTS_DEFAULT))
670                                 existing_config = 1;
671                         res = 0;
672                 }
673         }
674
675 #ifdef MENUSELECT_DEBUG
676         /* Dump the list produced by parsing the various input files */
677         dump_member_list();
678 #endif
679
680         if (!existing_config)
681                 process_defaults();
682         else if (check_deps)
683                 res = sanity_check();
684
685         /* Run the menu to let the user enable/disable options */
686         if (!check_deps && !res)
687                 res = run_menu();
688
689         /* Write out the menuselect.makeopts file if
690          * 1) menuselect was not executed with --check-deps
691          * 2) menuselect was executed with --check-deps but menuselect.makeopts
692          *    did not already exist.
693          */
694         if ((!check_deps || !existing_config) && !res)
695                 res = generate_makeopts_file();
696         
697         /* free everything we allocated */
698         free_trees();
699         free_member_list();
700
701         if (check_deps && !existing_config && !res) {
702                 fprintf(stderr, "\n***********************************************************\n");
703                 fprintf(stderr, "* menuselect.makeopts file generated with default values! *\n");
704                 fprintf(stderr, "* Please rerun make to build Asterisk.                    *\n");
705                 fprintf(stderr, "***********************************************************\n\n");
706                 res = -1;
707         }
708
709         /* In some cases, such as modifying the CFLAGS for the build,
710          * a "make clean" needs to be forced.  Removing the .lastclean 
711          * file does this. */
712         if (force_clean)
713                 unlink(".lastclean");
714
715         exit(res);
716 }