1 /* $NetBSD: rcorder.c,v 1.13 2004/09/21 15:47:32 rumble Exp $ */ 2 3 /* 4 * Copyright (c) 1998, 1999 Matthew R. Green 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 3. The name of the author may not be used to endorse or promote products 16 * derived from this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 19 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 22 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 23 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 30 /* 31 * Copyright (c) 1998 32 * Perry E. Metzger. All rights reserved. 33 * 34 * Redistribution and use in source and binary forms, with or without 35 * modification, are permitted provided that the following conditions 36 * are met: 37 * 1. Redistributions of source code must retain the above copyright 38 * notice, this list of conditions and the following disclaimer. 39 * 2. Redistributions in binary form must reproduce the above copyright 40 * notice, this list of conditions and the following disclaimer in the 41 * documentation and/or other materials provided with the distribution. 42 * 3. All advertising materials mentioning features or use of this software 43 * must display the following acknowledgement: 44 * This product includes software developed for the NetBSD Project 45 * by Perry E. Metzger. 46 * 4. The name of the author may not be used to endorse or promote products 47 * derived from this software without specific prior written permission. 48 * 49 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 50 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 51 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 52 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 53 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 54 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 55 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 56 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 57 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 58 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 59 */ 60 61 #include <sys/types.h> 62 #include <sys/stat.h> 63 64 #include <err.h> 65 #include <stdio.h> 66 #include <stdlib.h> 67 #include <string.h> 68 #include <unistd.h> 69 70 #include "ealloc.h" 71 #include "hash.h" 72 73 #ifdef DEBUG 74 int debug = 0; 75 # define DPRINTF(args) if (debug) { fflush(stdout); fprintf args; } 76 #else 77 # define DPRINTF(args) 78 #endif 79 80 #define REQUIRE_STR "# REQUIRE:" 81 #define REQUIRE_LEN (sizeof(REQUIRE_STR) - 1) 82 #define REQUIRES_STR "# REQUIRES:" 83 #define REQUIRES_LEN (sizeof(REQUIRES_STR) - 1) 84 #define PROVIDE_STR "# PROVIDE:" 85 #define PROVIDE_LEN (sizeof(PROVIDE_STR) - 1) 86 #define PROVIDES_STR "# PROVIDES:" 87 #define PROVIDES_LEN (sizeof(PROVIDES_STR) - 1) 88 #define BEFORE_STR "# BEFORE:" 89 #define BEFORE_LEN (sizeof(BEFORE_STR) - 1) 90 #define KEYWORD_STR "# KEYWORD:" 91 #define KEYWORD_LEN (sizeof(KEYWORD_STR) - 1) 92 #define KEYWORDS_STR "# KEYWORDS:" 93 #define KEYWORDS_LEN (sizeof(KEYWORDS_STR) - 1) 94 95 int exit_code; 96 int file_count; 97 char **file_list; 98 99 enum { 100 RESET = 0, 101 SET = 1, 102 }; 103 104 Hash_Table provide_hash_s, *provide_hash; 105 106 typedef struct provnode provnode; 107 typedef struct filenode filenode; 108 typedef struct f_provnode f_provnode; 109 typedef struct f_reqnode f_reqnode; 110 typedef struct strnodelist strnodelist; 111 112 struct provnode { 113 int head; 114 int in_progress; 115 filenode *fnode; 116 provnode *next, *last; 117 }; 118 119 struct f_provnode { 120 provnode *pnode; 121 f_provnode *next; 122 }; 123 124 struct f_reqnode { 125 Hash_Entry *entry; 126 f_reqnode *next; 127 }; 128 129 struct strnodelist { 130 filenode *node; 131 strnodelist *next; 132 char s[1]; 133 }; 134 135 struct filenode { 136 char *filename; 137 int in_progress; 138 filenode *next, *last; 139 f_reqnode *req_list; 140 f_provnode *prov_list; 141 strnodelist *keyword_list; 142 }; 143 144 filenode fn_head_s, *fn_head; 145 146 strnodelist *bl_list; 147 strnodelist *keep_list; 148 strnodelist *skip_list; 149 150 void do_file(filenode *fnode); 151 void strnode_add(strnodelist **, char *, filenode *); 152 int skip_ok(filenode *fnode); 153 int keep_ok(filenode *fnode); 154 void satisfy_req(f_reqnode *rnode, char *); 155 void crunch_file(char *); 156 void parse_line(filenode *, char *, void (*)(filenode *, char *)); 157 filenode *filenode_new(char *); 158 void add_require(filenode *, char *); 159 void add_provide(filenode *, char *); 160 void add_before(filenode *, char *); 161 void add_keyword(filenode *, char *); 162 void insert_before(void); 163 Hash_Entry *make_fake_provision(filenode *); 164 void crunch_all_files(void); 165 void initialize(void); 166 void generate_ordering(void); 167 int main(int, char *[]); 168 169 int 170 main(int argc, char *argv[]) 171 { 172 int ch; 173 174 while ((ch = getopt(argc, argv, "dk:s:")) != -1) 175 switch (ch) { 176 case 'd': 177 #ifdef DEBUG 178 debug = 1; 179 #else 180 warnx("debugging not compiled in, -d ignored"); 181 #endif 182 break; 183 case 'k': 184 strnode_add(&keep_list, optarg, 0); 185 break; 186 case 's': 187 strnode_add(&skip_list, optarg, 0); 188 break; 189 default: 190 /* XXX should crunch it? */ 191 break; 192 } 193 argc -= optind; 194 argv += optind; 195 196 file_count = argc; 197 file_list = argv; 198 199 DPRINTF((stderr, "parse_args\n")); 200 initialize(); 201 DPRINTF((stderr, "initialize\n")); 202 crunch_all_files(); 203 DPRINTF((stderr, "crunch_all_files\n")); 204 generate_ordering(); 205 DPRINTF((stderr, "generate_ordering\n")); 206 207 exit(exit_code); 208 } 209 210 /* 211 * initialise various variables. 212 */ 213 void 214 initialize(void) 215 { 216 217 fn_head = &fn_head_s; 218 219 provide_hash = &provide_hash_s; 220 Hash_InitTable(provide_hash, file_count); 221 } 222 223 /* generic function to insert a new strnodelist element */ 224 void 225 strnode_add(strnodelist **listp, char *s, filenode *fnode) 226 { 227 strnodelist *ent; 228 229 ent = emalloc(sizeof *ent + strlen(s)); 230 ent->node = fnode; 231 strcpy(ent->s, s); 232 ent->next = *listp; 233 *listp = ent; 234 } 235 236 /* 237 * below are the functions that deal with creating the lists 238 * from the filename's given and the dependancies and provisions 239 * in each of these files. no ordering or checking is done here. 240 */ 241 242 /* 243 * we have a new filename, create a new filenode structure. 244 * fill in the bits, and put it in the filenode linked list 245 */ 246 filenode * 247 filenode_new(char *filename) 248 { 249 filenode *temp; 250 251 temp = emalloc(sizeof(*temp)); 252 memset(temp, 0, sizeof(*temp)); 253 temp->filename = estrdup(filename); 254 temp->req_list = NULL; 255 temp->prov_list = NULL; 256 temp->keyword_list = NULL; 257 temp->in_progress = RESET; 258 /* 259 * link the filenode into the list of filenodes. 260 * note that the double linking means we can delete a 261 * filenode without searching for where it belongs. 262 */ 263 temp->next = fn_head->next; 264 if (temp->next != NULL) 265 temp->next->last = temp; 266 temp->last = fn_head; 267 fn_head->next = temp; 268 return (temp); 269 } 270 271 /* 272 * add a requirement to a filenode. 273 */ 274 void 275 add_require(filenode *fnode, char *s) 276 { 277 Hash_Entry *entry; 278 f_reqnode *rnode; 279 int new; 280 281 entry = Hash_CreateEntry(provide_hash, s, &new); 282 if (new) 283 Hash_SetValue(entry, NULL); 284 rnode = emalloc(sizeof(*rnode)); 285 rnode->entry = entry; 286 rnode->next = fnode->req_list; 287 fnode->req_list = rnode; 288 } 289 290 /* 291 * add a provision to a filenode. if this provision doesn't 292 * have a head node, create one here. 293 */ 294 void 295 add_provide(filenode *fnode, char *s) 296 { 297 Hash_Entry *entry; 298 f_provnode *f_pnode; 299 provnode *pnode, *head; 300 int new; 301 302 entry = Hash_CreateEntry(provide_hash, s, &new); 303 head = Hash_GetValue(entry); 304 305 /* create a head node if necessary. */ 306 if (head == NULL) { 307 head = emalloc(sizeof(*head)); 308 head->head = SET; 309 head->in_progress = RESET; 310 head->fnode = NULL; 311 head->last = head->next = NULL; 312 Hash_SetValue(entry, head); 313 } 314 #if 0 315 /* 316 * Don't warn about this. We want to be able to support 317 * scripts that do two complex things: 318 * 319 * - Two independent scripts which both provide the 320 * same thing. Both scripts must be executed in 321 * any order to meet the barrier. An example: 322 * 323 * Script 1: 324 * 325 * PROVIDE: mail 326 * REQUIRE: LOGIN 327 * 328 * Script 2: 329 * 330 * PROVIDE: mail 331 * REQUIRE: LOGIN 332 * 333 * - Two interdependent scripts which both provide the 334 * same thing. Both scripts must be executed in 335 * graph order to meet the barrier. An example: 336 * 337 * Script 1: 338 * 339 * PROVIDE: nameservice dnscache 340 * REQUIRE: SERVERS 341 * 342 * Script 2: 343 * 344 * PROVIDE: nameservice nscd 345 * REQUIRE: dnscache 346 */ 347 else if (new == 0) { 348 warnx("file `%s' provides `%s'.", fnode->filename, s); 349 warnx("\tpreviously seen in `%s'.", 350 head->next->fnode->filename); 351 } 352 #endif 353 354 pnode = emalloc(sizeof(*pnode)); 355 pnode->head = RESET; 356 pnode->in_progress = RESET; 357 pnode->fnode = fnode; 358 pnode->next = head->next; 359 pnode->last = head; 360 head->next = pnode; 361 if (pnode->next != NULL) 362 pnode->next->last = pnode; 363 364 f_pnode = emalloc(sizeof(*f_pnode)); 365 f_pnode->pnode = pnode; 366 f_pnode->next = fnode->prov_list; 367 fnode->prov_list = f_pnode; 368 } 369 370 /* 371 * put the BEFORE: lines to a list and handle them later. 372 */ 373 void 374 add_before(filenode *fnode, char *s) 375 { 376 377 strnode_add(&bl_list, s, fnode); 378 } 379 380 /* 381 * add a key to a filenode. 382 */ 383 void 384 add_keyword(filenode *fnode, char *s) 385 { 386 387 strnode_add(&fnode->keyword_list, s, fnode); 388 } 389 390 /* 391 * loop over the rest of a line, giving each word to 392 * add_func() to do the real work. 393 */ 394 void 395 parse_line(filenode *node, char *buffer, void (*add_func)(filenode *, char *)) 396 { 397 char *s; 398 399 while ((s = strsep(&buffer, " \t\n")) != NULL) 400 if (*s != '\0') 401 (*add_func)(node, s); 402 } 403 404 /* 405 * given a file name, create a filenode for it, read in lines looking 406 * for provision and requirement lines, building the graphs as needed. 407 */ 408 void 409 crunch_file(char *filename) 410 { 411 FILE *fp; 412 char *buf; 413 int require_flag, provide_flag, before_flag, keyword_flag; 414 enum { BEFORE_PARSING, PARSING, PARSING_DONE } state; 415 filenode *node; 416 char delims[3] = { '\\', '\\', '\0' }; 417 struct stat st; 418 419 if ((fp = fopen(filename, "r")) == NULL) { 420 warn("could not open %s", filename); 421 return; 422 } 423 424 if (fstat(fileno(fp), &st) == -1) { 425 warn("could not stat %s", filename); 426 fclose(fp); 427 return; 428 } 429 430 if (!S_ISREG(st.st_mode)) { 431 #if 0 432 warnx("%s is not a file", filename); 433 #endif 434 fclose(fp); 435 return; 436 } 437 438 node = filenode_new(filename); 439 440 /* 441 * we don't care about length, line number, don't want # for comments, 442 * and have no flags. 443 */ 444 for (state = BEFORE_PARSING; state != PARSING_DONE && 445 (buf = fparseln(fp, NULL, NULL, delims, 0)) != NULL; free(buf)) { 446 require_flag = provide_flag = before_flag = keyword_flag = 0; 447 if (strncmp(REQUIRE_STR, buf, REQUIRE_LEN) == 0) 448 require_flag = REQUIRE_LEN; 449 else if (strncmp(REQUIRES_STR, buf, REQUIRES_LEN) == 0) 450 require_flag = REQUIRES_LEN; 451 else if (strncmp(PROVIDE_STR, buf, PROVIDE_LEN) == 0) 452 provide_flag = PROVIDE_LEN; 453 else if (strncmp(PROVIDES_STR, buf, PROVIDES_LEN) == 0) 454 provide_flag = PROVIDES_LEN; 455 else if (strncmp(BEFORE_STR, buf, BEFORE_LEN) == 0) 456 before_flag = BEFORE_LEN; 457 else if (strncmp(KEYWORD_STR, buf, KEYWORD_LEN) == 0) 458 keyword_flag = KEYWORD_LEN; 459 else if (strncmp(KEYWORDS_STR, buf, KEYWORDS_LEN) == 0) 460 keyword_flag = KEYWORDS_LEN; 461 else { 462 if (state == PARSING) 463 state = PARSING_DONE; 464 continue; 465 } 466 467 state = PARSING; 468 if (require_flag) 469 parse_line(node, buf + require_flag, add_require); 470 else if (provide_flag) 471 parse_line(node, buf + provide_flag, add_provide); 472 else if (before_flag) 473 parse_line(node, buf + before_flag, add_before); 474 else if (keyword_flag) 475 parse_line(node, buf + keyword_flag, add_keyword); 476 } 477 fclose(fp); 478 } 479 480 Hash_Entry * 481 make_fake_provision(filenode *node) 482 { 483 Hash_Entry *entry; 484 f_provnode *f_pnode; 485 provnode *head, *pnode; 486 static int i = 0; 487 int new; 488 char buffer[30]; 489 490 do { 491 snprintf(buffer, sizeof buffer, "fake_prov_%08d", i++); 492 entry = Hash_CreateEntry(provide_hash, buffer, &new); 493 } while (new == 0); 494 head = emalloc(sizeof(*head)); 495 head->head = SET; 496 head->in_progress = RESET; 497 head->fnode = NULL; 498 head->last = head->next = NULL; 499 Hash_SetValue(entry, head); 500 501 pnode = emalloc(sizeof(*pnode)); 502 pnode->head = RESET; 503 pnode->in_progress = RESET; 504 pnode->fnode = node; 505 pnode->next = head->next; 506 pnode->last = head; 507 head->next = pnode; 508 if (pnode->next != NULL) 509 pnode->next->last = pnode; 510 511 f_pnode = emalloc(sizeof(*f_pnode)); 512 f_pnode->pnode = pnode; 513 f_pnode->next = node->prov_list; 514 node->prov_list = f_pnode; 515 516 return (entry); 517 } 518 519 /* 520 * go through the BEFORE list, inserting requirements into the graph(s) 521 * as required. in the before list, for each entry B, we have a file F 522 * and a string S. we create a "fake" provision (P) that F provides. 523 * for each entry in the provision list for S, add a requirement to 524 * that provisions filenode for P. 525 */ 526 void 527 insert_before(void) 528 { 529 Hash_Entry *entry, *fake_prov_entry; 530 provnode *pnode; 531 f_reqnode *rnode; 532 strnodelist *bl; 533 int new; 534 535 while (bl_list != NULL) { 536 bl = bl_list->next; 537 538 fake_prov_entry = make_fake_provision(bl_list->node); 539 540 entry = Hash_CreateEntry(provide_hash, bl_list->s, &new); 541 if (new == 1) 542 warnx("file `%s' is before unknown provision `%s'", 543 bl_list->node->filename, bl_list->s); 544 545 for (pnode = Hash_GetValue(entry); pnode; pnode = pnode->next) { 546 if (pnode->head) 547 continue; 548 549 rnode = emalloc(sizeof(*rnode)); 550 rnode->entry = fake_prov_entry; 551 rnode->next = pnode->fnode->req_list; 552 pnode->fnode->req_list = rnode; 553 } 554 555 free(bl_list); 556 bl_list = bl; 557 } 558 } 559 560 /* 561 * loop over all the files calling crunch_file() on them to do the 562 * real work. after we have built all the nodes, insert the BEFORE: 563 * lines into graph(s). 564 */ 565 void 566 crunch_all_files(void) 567 { 568 int i; 569 570 for (i = 0; i < file_count; i++) 571 crunch_file(file_list[i]); 572 insert_before(); 573 } 574 575 /* 576 * below are the functions that traverse the graphs we have built 577 * finding out the desired ordering, printing each file in turn. 578 * if missing requirements, or cyclic graphs are detected, a 579 * warning will be issued, and we will continue on.. 580 */ 581 582 /* 583 * given a requirement node (in a filename) we attempt to satisfy it. 584 * we do some sanity checking first, to ensure that we have providers, 585 * aren't already satisfied and aren't already being satisfied (ie, 586 * cyclic). if we pass all this, we loop over the provision list 587 * calling do_file() (enter recursion) for each filenode in this 588 * provision. 589 */ 590 void 591 satisfy_req(f_reqnode *rnode, char *filename) 592 { 593 Hash_Entry *entry; 594 provnode *head; 595 596 entry = rnode->entry; 597 head = Hash_GetValue(entry); 598 599 if (head == NULL) { 600 warnx("requirement `%s' in file `%s' has no providers.", 601 Hash_GetKey(entry), filename); 602 exit_code = 1; 603 return; 604 } 605 606 /* return if the requirement is already satisfied. */ 607 if (head->next == NULL) 608 return; 609 610 /* 611 * if list is marked as in progress, 612 * print that there is a circular dependency on it and abort 613 */ 614 if (head->in_progress == SET) { 615 warnx("Circular dependency on provision `%s' in file `%s'.", 616 Hash_GetKey(entry), filename); 617 exit_code = 1; 618 return; 619 } 620 621 head->in_progress = SET; 622 623 /* 624 * while provision_list is not empty 625 * do_file(first_member_of(provision_list)); 626 */ 627 while (head->next != NULL) 628 do_file(head->next->fnode); 629 } 630 631 int 632 skip_ok(filenode *fnode) 633 { 634 strnodelist *s; 635 strnodelist *k; 636 637 for (s = skip_list; s; s = s->next) 638 for (k = fnode->keyword_list; k; k = k->next) 639 if (strcmp(k->s, s->s) == 0) 640 return (0); 641 642 return (1); 643 } 644 645 int 646 keep_ok(filenode *fnode) 647 { 648 strnodelist *s; 649 strnodelist *k; 650 651 for (s = keep_list; s; s = s->next) 652 for (k = fnode->keyword_list; k; k = k->next) 653 if (strcmp(k->s, s->s) == 0) 654 return (1); 655 656 /* an empty keep_list means every one */ 657 return (!keep_list); 658 } 659 660 /* 661 * given a filenode, we ensure we are not a cyclic graph. if this 662 * is ok, we loop over the filenodes requirements, calling satisfy_req() 663 * for each of them.. once we have done this, remove this filenode 664 * from each provision table, as we are now done. 665 */ 666 void 667 do_file(filenode *fnode) 668 { 669 f_reqnode *r, *r_tmp; 670 f_provnode *p, *p_tmp; 671 provnode *pnode; 672 int was_set; 673 674 DPRINTF((stderr, "do_file on %s.\n", fnode->filename)); 675 676 /* 677 * if fnode is marked as in progress, 678 * print that fnode; is circularly depended upon and abort. 679 */ 680 if (fnode->in_progress == SET) { 681 warnx("Circular dependency on file `%s'.", 682 fnode->filename); 683 was_set = exit_code = 1; 684 } else 685 was_set = 0; 686 687 /* mark fnode */ 688 fnode->in_progress = SET; 689 690 /* 691 * for each requirement of fnode -> r 692 * satisfy_req(r, filename) 693 */ 694 r = fnode->req_list; 695 while (r != NULL) { 696 r_tmp = r; 697 satisfy_req(r, fnode->filename); 698 r = r->next; 699 free(r_tmp); 700 } 701 fnode->req_list = NULL; 702 703 /* 704 * for each provision of fnode -> p 705 * remove fnode from provision list for p in hash table 706 */ 707 p = fnode->prov_list; 708 while (p != NULL) { 709 p_tmp = p; 710 pnode = p->pnode; 711 if (pnode->next != NULL) { 712 pnode->next->last = pnode->last; 713 } 714 if (pnode->last != NULL) { 715 pnode->last->next = pnode->next; 716 } 717 free(pnode); 718 p = p->next; 719 free(p_tmp); 720 } 721 fnode->prov_list = NULL; 722 723 /* do_it(fnode) */ 724 DPRINTF((stderr, "next do: ")); 725 726 /* if we were already in progress, don't print again */ 727 if (was_set == 0 && skip_ok(fnode) && keep_ok(fnode)) 728 printf("%s\n", fnode->filename); 729 730 if (fnode->next != NULL) { 731 fnode->next->last = fnode->last; 732 } 733 if (fnode->last != NULL) { 734 fnode->last->next = fnode->next; 735 } 736 737 DPRINTF((stderr, "nuking %s\n", fnode->filename)); 738 free(fnode->filename); 739 free(fnode); 740 } 741 742 void 743 generate_ordering(void) 744 { 745 746 /* 747 * while there remain undone files{f}, 748 * pick an arbitrary f, and do_file(f) 749 * Note that the first file in the file list is perfectly 750 * arbitrary, and easy to find, so we use that. 751 */ 752 753 /* 754 * N.B.: the file nodes "self delete" after they execute, so 755 * after each iteration of the loop, the head will be pointing 756 * to something totally different. The loop ends up being 757 * executed only once for every strongly connected set of 758 * nodes. 759 */ 760 while (fn_head->next != NULL) { 761 DPRINTF((stderr, "generate on %s\n", fn_head->next->filename)); 762 do_file(fn_head->next); 763 } 764 } 765