/*
 *  smoke16/src/ld.c - The SMOKE-16 linker.
 *  $Revision: 1.33 $
 *  Copyright 1997, Benjamin Sittler
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <smoke16/types.h>
#include <smoke16/libObj.h>
#include "getopt.h" /* argument parser */

#if defined(__GNUC__) && (__GNUC__ > 1)
#define unused __attribute__((unused))
#else
#define unused
#endif

static char unused RCSID[] = "$Id: ld.c,v 1.33 2001/08/28 18:01:48 bsittler Exp $";

#define SUMMARY \
   "Summary: %s [-help] [-version] [options] object-file...\n"

#define USAGE "\
Usage: %s [options] object-file...\n\
Options:\n\
-A vNUM                 link for SMOKE-16 toolset vNUM\n\
-E [0x]HEX              override the entry point\n\
-e SYMBOL               set the entry point to SYMBOL (default is '%s')\n\
-h or\n\
-help                   show this message and exit\n\
-i or -r                generate relocatable output\n\
-j or -jmagic           generate split I&D executable (JMAGIC)\n\
-L DIR                  add DIR to the archive search path\n\
-l ARCHIVE              link in libARCHIVE.a from the archive search path\n\
-N or -omagic           generate object or impure executable (OMAGIC)\n\
-n or -nmagic           generate pure executable (NMAGIC)\n\
-o EXE or\n\
-output-file EXE        name generated object file\n\
                        (if one object-file is given, the default is that\n\
                         object-file; otherwise the default is a.out)\n\
-S                      strip out debugging symbols\n\
-s                      strip out all symbols\n\
-Tbss [0x]HEX           set the load address of the BSS section\n\
-Tdata [0x]HEX          set the load address of the data section\n\
-T[text] [0x]HEX        set the load address of the text section\n\
-V or -version          print linker version and exit\n\
-Z or -lmagic           generate object archive (LMAGIC)\n\
-1                      process following archives once\n\
-2                      process following archive list twice\n\
"

#define VERSION \
   "SMOKE-16 linker $Revision: 1.33 $\n" \
   SMOKE16_WARRANTY

int main(int argc, char *argv[])
{
  struct filename {
    struct filename *next;
    char *name;
    int twice;
  } *first = 0, *last = 0, *current = 0,
      *first_dir = 0, *last_dir = 0, *current_dir = 0;
  int twice = 0;
  int Tbss_p = 0, Tdata_p = 0, Ttext_p = 0, Entry_p = 0;
  u16 Tbss = 0, Tdata = 0, Ttext = 0, Entry = 0;
  int Magic_p = 0;
  u16 Magic = 0;
  u16 toolversion = SMOKE16_TOOLVERSION;
  int toolversion_p = 0;
  char *entry = ENTRY_SYM;
  Sym *entry_sym = (Sym *)0;
  int strip_debug = 0;
  int strip_all = 0;
  Obj *object = (Obj *)0, *in_object = (Obj *)0;
  FILE *objfile = (FILE *)0;
  char *objfilename = "a.out";
  int objfilename_p = 0;
  int i, option_index;
  int load = 1;
  static struct option long_options[] = {
    {"help", 0, 0, 'h'},
    {"output-file", 1, 0, 'o'},
    {"version", 0, 0, 'V'},
    {"Tbss", 1, 0, 0},
    {"Tdata", 1, 0, 0},
    {"Ttext", 1, 0, 'T'},
    {"omagic", 0, 0, 'N'},
    {"nmagic", 0, 0, 'n'},
    {"jmagic", 0, 0, 'j'},
    {"lmagic", 0, 0, 'Z'},
    {0, 0, 0, 0}
  };

  while((i = getopt_long_only(argc, argv, "-A:e:hiL:l:o:rSsE:T:VNnjZ12", long_options, &option_index)) != -1)
    switch(i) {
    case 0:
      switch(option_index) {
      case 3:
	Tbss_p = 1;
	if (optarg[0] == '0' && ((optarg[1] == 'x') || (optarg[1] == 'X')))
	  optarg += 2;
	if (sscanf(optarg, "%" FMT_x16 "%s", &Tbss, optarg) != 1)
	  fprintf(stderr,
		  "%s: %s: not a valid hexadecimal number\n",
		  argv[0],
		  optarg),
	  exit(1);
	break;
      case 4:
	Tdata_p = 1;
	if (optarg[0] == '0' && ((optarg[1] == 'x') || (optarg[1] == 'X')))
	  optarg += 2;
	if (sscanf(optarg, "%" FMT_x16 "%s", &Tdata, optarg) != 1)
	  fprintf(stderr,
		  "%s: %s: not a valid hexadecimal number\n",
		  argv[0],
		  optarg),
	  exit(1);
	break;
      default:
        fprintf(stderr, SUMMARY, argv[0]);
        exit(2);
      }
      break;
    case 'A':
      if (*optarg != 'v')
	fprintf(stderr,
		"%s: %s: not a valid version\n",
		argv[0],
		optarg),
	  exit(1);
      optarg++;
      if (sscanf(optarg, "%" FMT_u16 "%s", &toolversion, optarg) != 1)
	fprintf(stderr,
		"%s: %s: not a valid number\n",
		argv[0],
		optarg),
	  exit(1);
      if (toolversion > SMOKE16_TOOLVERSION)
	fprintf(stderr,
		"%s: toolset version must be between 0 and %" FMT_u16 "\n",
		argv[0],
		SMOKE16_TOOLVERSION),
	  exit(1);
      toolversion_p = 1;
      break;
    case 'e':
      Entry_p = 0;
      entry = optarg;
      break;
    case 'h':
      printf(USAGE, argv[0], entry);
      exit(0);
      break;
    case 'o':
      objfilename_p = 1;
      objfilename = (char *)optarg;
      if (objfilename[0] == '-' && objfilename[1] == '\0') {
	objfilename = "stdout";
	objfile = stdout;
      } else
	objfile = (FILE *)0;
      break;
    case 'i':
    case 'r':
      if (! Magic_p) {
	Magic = SMOKE16_OMAGIC;
	Magic_p = 1;
      }
      load = 0;
      break;
    case 'S':
      strip_debug = 1;
      break;
    case 's':
      strip_all = 1;
      break;
    case 'E':
      Entry_p = 1;
      if (optarg[0] == '0' && ((optarg[1] == 'x') || (optarg[1] == 'X')))
	optarg += 2;
      if (sscanf(optarg, "%" FMT_x16 "%s", &Entry, optarg) != 1)
	fprintf(stderr,
		"%s: %s: not a valid hexadecimal number\n",
		argv[0],
		optarg),
	  exit(1);
      break;
    case 'T':
      Ttext_p = 1;
      if (optarg[0] == '0' && ((optarg[1] == 'x') || (optarg[1] == 'X')))
	optarg += 2;
      if (sscanf(optarg, "%" FMT_x16 "%s", &Ttext, optarg) != 1)
	fprintf(stderr,
		"%s: %s: not a valid hexadecimal number\n",
		argv[0],
		optarg),
	  exit(1);
      break;
    case 'V':
      printf(VERSION, SMOKE16_TOOLVERSION);
      exit(0);
      break;
    case 'N':
      Magic_p = 1;
      Magic = SMOKE16_OMAGIC;
      break;
    case 'n':
      Magic_p = 1;
      Magic = SMOKE16_NMAGIC;
      break;
    case 'j':
      Magic_p = 1;
      Magic = SMOKE16_JMAGIC;
      break;
    case 'Z':
      Magic_p = 1;
      Magic = SMOKE16_LMAGIC;
      load = 0;
      break;
    case '1':
      twice = 0;
      break;
    case '2':
      twice = 1;
      break;
    case 'L':
      if (! (current_dir = (struct filename *) malloc(sizeof(*current_dir)))) {
	perror("malloc");
	exit(1);
      }
      current_dir -> name = optarg;
      current_dir -> next = 0;
      current_dir -> twice = 0;
      if (! first_dir)
	first_dir = current_dir;
      else
	last_dir -> next = current_dir;
      last_dir = current_dir;
      break;
    case 'l':
      {
	char *basename;
	size_t baselen;
	char *fullname = 0;
	struct stat buf;

	/* convert "ARCHIVE" to "libARCHIVE.a" */
	baselen = strlen("lib") + strlen(optarg) + strlen(".a") + 1;
	if (! (basename = (char *) malloc(baselen))) {
	  perror("malloc");
	  exit(1);
	}
	strcpy(basename, "lib");
	strcat(basename, optarg);
	strcat(basename, ".a");

	/* search for it on the archive search path */
	for (current_dir = first_dir;
	     current_dir;
	     current_dir = current_dir -> next) {
	  if (! (fullname = (char *)
                 malloc(strlen(current_dir -> name) + strlen("/") + baselen + 1))) {
	    perror("malloc");
	    exit(1);
	  }
	  strcpy(fullname, current_dir -> name);
	  if (fullname[1] &&
	      (fullname[strlen(fullname) - 1] != '/'))
	    strcat(fullname, "/");
	  strcat(fullname, basename);
	  if (! stat(fullname, &buf))
	    break;
	  free(fullname);
	  fullname = 0;
	}

	/* failing that, check the current directory */
	if ((! fullname) && ! stat(basename, &buf))
	  fullname = basename;
	else
	  free(basename);

	/* as a last resort, use "-lARCHIVE" */
	if (! fullname) {
	  if (! (fullname = (char *) malloc(strlen("-l") + strlen(optarg) + 1))) {
	    perror("malloc");
	    exit(1);
	  }
	  strcpy(fullname, "-l");
	  strcat(fullname, optarg);
	}

	if (! (current = (struct filename *) malloc(sizeof(*current)))) {
	  perror("malloc");
	  exit(1);
	}
	current -> name = fullname;
	current -> next = 0;
	current -> twice = twice;
	if (! first)
	  first = current;
	else
	  last -> next = current;
	last = current;
      }
      break;
    case 1:
      if (! (current = (struct filename *) malloc(sizeof(*current)))) {
	perror("malloc");
	exit(1);
      }
      current -> name = optarg;
      current -> next = 0;
      current -> twice = twice;
      if (! first)
	first = current;
      else
	last -> next = current;
      last = current;
      break;
    default:
      fprintf(stderr, SUMMARY, argv[0]);
      exit(2);
    }
  while (argc > optind) {
    if (! (current = (struct filename *) malloc(sizeof(*current)))) {
      perror("malloc");
      exit(1);
    }
    current -> name = argv[optind ++];
    current -> next = 0;
    current -> twice = twice;
    if (! first)
      first = current;
    else
      last -> next = current;
    last = current;
  }
  if ((first != last) || Magic_p || toolversion_p) {
    if (! (object = newObj(objfilename))) {
      fprintf(stderr,
	      "%s: failed to create new object file\n",
	      objfilename);
      exit(1);
    }
    if (Magic_p)
      object -> magic = Magic;
    if (toolversion_p)
      object -> toolversion = toolversion;
  }
  if ((current = first))
    do {
      FILE *infile;
      char *name;
      
      if ((current -> name[0] == '-') &&
	  (current -> name[1] == '\0')) {
	infile = stdin;
	name = "stdin";
      } else {
	infile = fopen(current -> name, "rb");
	name = current -> name;
      }
      if (infile) {
	struct stat buf;

	if (!(in_object = readObj(name, infile))) {
	  fprintf(stderr,
		  "%s: failed to read object file\n",
		  name);
	  if (object)
	    freeObj(object);
	  exit(1);
	}
	if (! fstat(fileno(infile), &buf)) {
	  /* FIXME: this should include owner and mode too! */
	  in_object -> mtime = buf . st_mtime;
	}

	/* when linking in libraries, do things differently */
	if ((Magic != SMOKE16_LMAGIC) &&
	    (in_object -> magic == SMOKE16_LMAGIC)) {

	  /* if a library is the first input file, prepend a dummy object to the list */
	  if ((! object) &&
	      ! (object = newObj(objfilename))) {
	    fprintf(stderr,
		    "%s: failed to create new object file\n",
		    objfilename);
	    freeObj(in_object);
	    exit(1);
	  }

	  /* process some libraries twice, as a convenience to the user */
	  if (current -> twice) {
	    if (! (last -> next = (struct filename *) malloc(sizeof(*last)))) {
	      perror("malloc");
	      freeObj(in_object);
	      freeObj(object);
	      exit(1);
	    }
	    last = last -> next;
	    last -> name = current -> name;
	    last -> next = 0;
	    last -> twice = 0;
	  }
	}

	/* the first input may be the only object */
	if (! object) {
	  object = in_object;
	  if (first == last) {
	    char *s;

	    if (! objfilename_p) {
	      objfilename = current -> name;
	      if ((objfilename[0] == '-') &&
		  (objfilename[1] == '\0')) {
		objfilename = "stdout";
		objfile = stdout;
	      }
	    }
	    if ((s = strdup(objfilename))) {
	      free (object -> name);
	      object -> name = s;
	    }
	  }
	}
	if (load && (! entry_sym) && (! Entry_p)) {
	  if (! (entry_sym = newSym(entry, SMOKE16_N_UNDF | SMOKE16_N_EXT, 0, 0, 0))) {
	    fprintf(stderr,
		    "%s: failed to create entry symbol `%s'\n",
		    objfilename, entry);
	    freeObj(object);
	    exit(1);
	  }
	  if (! (entry_sym = enterinSymTab(object -> tab, entry_sym))) {
	    fprintf(stderr,
		    "%s: failed to enter `%s' in symbol table\n",
		    objfilename, entry);
	    freeObj(object);
	    exit(1);
	  }
	}
	if (object == in_object)
	  ;
	else if (linkObj(object, in_object)) {
	  fprintf(stderr,
		  "%s: failed to link in %s\n",
		  object->name,
		  in_object->name);
	  freeObj(in_object);
	  freeObj(object);
	  exit(1);
	} else
	  freeObj(in_object);
      } else {
	perror(name);
	if (object)
	  freeObj(object);
	exit(1);
      }
      if (infile != stdin)
	fclose(infile);
    } while ((current = current -> next));
  else {
    fprintf(stderr, "%s: no input files.\n", argv[0]);
    if (object)
      freeObj(object);
    exit(2);
  }
  if (load) {
    if ((object -> magic == SMOKE16_OMAGIC) &&
	! Magic_p)
      object -> magic = SMOKE16_NMAGIC;
    if ((object -> magic == SMOKE16_JMAGIC) &&
	! Tdata_p) {
      Tdata_p = 1;
      Tdata = (object -> toolversion == 0U)
	? 0x0000U
	: 0x0400U;
    }
    if (loadObj(object,
		Entry_p, Entry,
		Ttext_p, Ttext,
		Tdata_p, Tdata,
		Tbss_p, Tbss,
		entry)) {
      fprintf(stderr, "%s: cannot load\n", objfilename);
      if (object)
	freeObj(object);
      exit(1);
    }
  }
  if (strip_all || strip_debug)
    if (stripObj(object, strip_all)) {
      fprintf(stderr, "%s: cannot strip\n", objfilename);
      if (object)
	freeObj(object);
      exit(1);
    }
  if (!objfile)
    if (!(objfile = fopen(objfilename, "wb"))) {
      fputs(objfilename, stderr);
      perror(": cannot open file for writing");
      if (object)
	freeObj(object);
      exit(1);
    }
  if (writeObj(object, objfile)) {
    fprintf(stderr, "%s: failed to write object file\n", objfilename);
    if (fclose(objfile)) {
      fputs(objfilename, stderr);
      perror(": cannot close");
    }
    if (objfile != stdout)
      if (unlink(objfilename)) {
	fputs(objfilename, stderr);
	perror(": removal of incomplete object file failed");
      }
    if (object)
      freeObj(object);
    exit(1);
  } else if (objfile != stdout) {
    int mask;
    
    umask((mask = umask(0077)));
    if (fclose(objfile)) {
      fputs(objfilename, stderr);
      perror(": cannot close");
      if (object)
	freeObj(object);
      exit(1);
    }
    if (load)
      if (chmod(objfilename, 0777 & ~mask))
	fputs(objfilename, stderr),
	perror(": warning: cannot change mode");
  }
  if (object)
    freeObj(object);
  return 0;
}
