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