#!/usr/bin/env pike
// -*- Pike -*-

// RNTCL, a front-end to MS VC++ and Intel ICL/ECL with options similar to GCC
// Written by Fredrik Hubinette & Henrik Grubbstrm.

inherit "lib.pike";

// #define WIN64

// Define this to use the old code for deciding flags for cl.exe. It
// might help if you're using pre-VC8.
//#define OLD_VC

#ifdef ECL
#ifdef WIN64
constant compiler="ecl";
#else /* !WIN64 */
constant compiler="icl";
#endif /* WIN64 */
#else /* !ECL */
constant compiler="cl";
#endif /* ECL */

constant assembler="ias";

constant default_cflags = ({
  "-W3",	// More warnings.
#ifdef ECL
  "-Wp64",	// Generate warnings for loss of precision.
  //"-W4",	// Maximum verbosity.
#else /* !ECL */
#ifdef OLD_VC
  "-Zm300",	// Memory allocation limit
  "-Ge",	// Activate stack probes (deprecated in VC8)
  "-GR",	// Runtime type information (RTTI)
#else
  "-we4020",	// Make it an error if too many args are passed.
		// Configure tests depends on that.
  "-wd4996",	// No warnings about using posix functions not beginning with "_".
#endif
  "-F8388608",	// Stack size
#endif /* ECL */
#ifdef WIN64
  "-D__WIN64__",
  "-D_WIN64",
#else /* !WIN64 */
  "-D__WIN32__",
  "-D_WIN32",
#endif /* WIN64 */
  "-D__NT__",
  "-D_CRT_SECURE_NO_DEPRECATE",	// Avoid warnings about insecure functions in VC8.
//    "-D__STDC__",  
  "-nologo",	// Suppress banner
});

// Globs for flags that have to be removed for different VC versions
// to work. Use an environment variable RNTCL_VC={6,7,8} to control
// the compat level.
constant removeflags_vc6 = ({"-we*", "-wd*"});
constant removeflags_vc7 = ({}); // FIXME: Not verified.
constant removeflags_vc8 = ({"-GB"});
constant removeflags_vc9 = ({"-GB"}); //FIXME: Works. But only copied from VC8.

int verbose=0;

// Temporary variable
int linking_failed;

// Files to remove upon exit
array(string) tmpfiles=({});

void exit(int code)
{
  if(getenv("CLEANUP")!="no")
    Array.map(tmpfiles,rm);

  predef::exit(code);
}

string get_ext(string file)
{
  sscanf(file=lower_case(reverse(file)),"%s.",file);
  return reverse(file);
}

string remove_ext(string file)
{
  sscanf(file=reverse(file),"%*s.%s",file);
  return reverse(file);
}

string o_to_obj(string fname)
{
  if (fname[sizeof(fname)-2..] == ".o") {
    return fname + "bj";
  }
  return fname;
}

array|object paranoia_stat(string f)
{
  // Let the client have 10 seconds to flush the data...
  for (int i=0; i < 100; i++) {
    array|object st = file_stat(f);
    if (st) return st;
    sleep(0.1);
  }
  return file_stat(f);
}

void safe_hardlink (string from, string to)
{
  if(!paranoia_stat(from))
  {
    werror("RNTCL: output file not generated (%s in %s).\n", from, getcwd());
    exit(1);
  }
  rm(to);
  if (1)
//catch {
//      hardlink(from, to);
 //   }) 
{
    string s = Stdio.read_bytes(from);
    if (!s) {
      werror("RNTCL: Failed to read output file (%s in %s).\n", from, getcwd());
      exit(1);
    }
    if (Stdio.write_file(to, s) != sizeof(s)) {
      werror("RNTCL: Failed to write all of output file (%s in %s).\n", to, getcwd());
      exit(1);
    }
  }
}

int main(int argc, array(string) argv)
{
  //werror("%O\n",argv);
  array(string) ldopts = ({});
  array(string) cflags=default_cflags;
  array(string) libs=({});

  string target="exe";
  int debug,share,dynamic;
  string output, wantfile, crt_lib;

  array opts=Getopt.find_all_options(argv, aggregate(
    ({"oper_pre",Getopt.NO_ARG, ({"-E"}) }),
    ({"oper_asm",Getopt.NO_ARG, ({"-S"}) }),
    ({"oper_comp",Getopt.NO_ARG, ({"-c"}) }),	
    ({"verbose",Getopt.NO_ARG, ({"-v"}) }),	
    ({"debug",
#ifdef OLD_VC
      Getopt.MAY_HAVE_ARG,
#else
      Getopt.NO_ARG,
#endif
      ({"-g"}) }),
    ({"optimize",Getopt.MAY_HAVE_ARG, ({"-O"}) }),
    ({"inline",Getopt.MAY_HAVE_ARG, ({"-Q"}) }),
    ({"include",Getopt.HAS_ARG, ({"-I"}) }),
    ({"link",Getopt.HAS_ARG, ({"-l"}) }),
    ({"s",Getopt.MAY_HAVE_ARG, ({"-s"}) }),
#ifdef OLD_VC
    ({"ignore",Getopt.MAY_HAVE_ARG, ({"-t"}) }),
#endif
    ({"dynamic",Getopt.HAS_ARG, ({"-r"}) }),
    ({"ldpath",Getopt.HAS_ARG, ({"-L"}) }),
#ifdef OLD_VC
    ({"ignore",Getopt.HAS_ARG, ({"-R"}) }),
#endif
    ({"warn",Getopt.MAY_HAVE_ARG, ({"-W"}) }),
    ({"define",Getopt.HAS_ARG, ({"-D"}) }),
    ({"undefine",Getopt.HAS_ARG, ({"-U"})}),
    ({"output",Getopt.HAS_ARG, ({"-o"}) }),
    ({"export",Getopt.HAS_ARG, ({"--export"}) }),
    ({"M", Getopt.HAS_ARG, ({"-M"})}),
    ({"p", Getopt.HAS_ARG, ({"-p"})}),
    ));
  foreach(opts, array option)
    {
      // All the code below is oblivious to the fact that option[1] is
      // the integer 1 if a Getopt.MAY_HAVE_ARG lacks an arg. Let's
      // handle it without special cases.
      if (option[1] == 1) option[1] = "";

      switch(option[0])
      {
	case "dynamic":
	  // This doesn't work with VC8. Your best option is to use
	  // -shared instead.
	  dynamic++;
	  ldopts+=({
	    "-DELAY:nobind",
	      "-OPT:noref" // This might not be required
	      });
	  break;
	case "verbose":
	  verbose++;
	  ldopts+=({"-VERBOSE:lib"});
	  break;

	case "export": // fixme
//	  ldopts+=({"export",option[1]+"_"});
	  break;

	case "s":
	  switch(option[1])
	  {
	    case "hare":
	    case "hared":
	      share=1;
	      target="dll";
	      break;

	    case "tatic":
	      // Ignore this for compat with the uncompressor.c rules.
	      break;

	    default:
	      error ("Unknown option -s%s.\n", option[1]);
	  }
	  break;
	  
	case "oper_pre":
	  cflags+=({ "-E" });
	  target="-";
	  break;

        case "oper_asm":
	  cflags+=({ "-S" });
	  target="asm";
	  break;

	case "oper_comp":
	  target="obj";
	  cflags+=({ "-c" });
	  break;

	case "debug":
	  cflags+=({
#ifdef OLD_VC
	    "-Z7",   // This flag is of no use if -Zi is given.
#endif
	    "-Zi",   // This flag prevents parallell compilation
//	    "-FR",   // This flag confuses libtool
//	    "-Yd",
	  });
	  debug=1;
	  break;
	  
	case "optimize":
#ifdef OLD_VC
	  if (option[1] == "2") {
#ifdef WIN64
	      cflags += ({
		"-O"	// Optimization flags:
		"b2"	//   Inline any functions as the compiler sees fit
		// Disabled due to optimizer bugs in XP Platform SDK Beta 2.
		//"g"	//   Enable global optimizer
		"i"	//   Use intrinsic functions
		"t"	//   Favour fast code
	      });
#else /* !WIN64 */
	      cflags += ({
		"-O"	// Optimization flags:
		"b2"	//   Inline any functions as the compiler sees fit
		"g"	//   Enable global optimizer (deprecated in VC8)
		"i"	//   Use intrinsic functions
		"t"	//   Favour fast code
		"y",	//   Omit frame pointer
		"-GB",	// Optimize for Pentium (doesn't exist in VC8)
	      });
#endif /* !WIN64 */
	  } else
#endif	// OLD_VC
	  {
	    // In VC8, -O2 apparently enables more compact stack
	    // frames than the mantra of individual optimization flags
	    // above.
	    cflags += ({"-O" + option[1]});
	  }
	  break;

        case "inline":
	  if (option[1] == "ip") {
	    cflags+=({"-Qip"});
	  }
	  break;

	case "include":
	  // Avoid searching 'local' include dirs.
	  // This is not a very pretty solution.
	  if(sscanf(option[1],"/usr/include/%*s") ||
	     sscanf(option[1],"/usr/local/%*s"))
	    break;
	  
	  cflags+=({"-I"+fixpath(option[1])});
	  break;

	case "link":
	  // -lm and -lc are automatically handled ?
	  if(option[1]=="m" || option[1]=="c") break;

	  // We optimize a little, no need to bring in the same
	  // library many times in a row.
	  libs |= ({option[1]+".lib"});
	  break;
	  
        case "warn":
	  if(option[1])
	  {
	    // This allows us to pass options to the linker
	    if(sscanf(option[1],"l,%s",string tmp))
	    {
	      // This was done for my convenience, it can be taken
	      // out once smartlink has been fixed to not use absoute
	      // paths for the 'uname' binary.
	      if(sscanf(tmp,"-rpath%*s")) break;
	      
	      ldopts+=({tmp});
	      break;
	    }
	  }

	  // More options should be recognized, options which are not
	  // recognized should generate warning/error messages.
	  switch(option[1])
	  {
	    case "all": cflags+=({"-W4"}); break;
            // Not supported, but in the manual...
	    case "X": cflags+=({"-WX"}); break;
	    default: cflags+=({"-W3"}); break;
	  }
	  break;

        case "ldpath":
	  ldopts+=({"-LIBPATH:"+fixpath(option[1])});
	  break;
	  
	case "define": cflags+=({"-D"+option[1]}); break;
	case "undefine": cflags+=({"-U"+option[1]}); break;
	case "output":
	  output=option[1];
	  break;

	case "M":
	  cflags += ({"-M" + option[1]});
	  switch (option[1]) {
	    case "T": crt_lib = "libcmt.lib"; break;
	    case "D": crt_lib = "msvcrt.lib"; break;
	    case "Td": crt_lib = "libcmtd.lib"; break;
	    case "Dd": crt_lib = "msvcrtd.lib"; break;
	    default: error ("Unknown option -M%s\n", option[1]); break;
	  }
	  break;

	case "p":
	  if (has_prefix (option[1], "db:"))
	    ldopts += ({"-p" + option[1]});
	  else
	    error ("Unknown option -p%s\n", option[1]);
	  break;
      }
    }

  // Scan through the remaining arguments
  argv=Getopt.get_args(argv);

  string compile_source;
  foreach (argv, string arg)
    if (has_suffix (arg, ".c")) {
      compile_source = (arg / "/")[-1];
      break;
    }

  cflags+=Array.map(Array.map(argv[1..],fixpath), o_to_obj);

  foreach(argv[1..], string tmp)
  {
    if(tmp[0]=='-' || tmp[0] == '+')
    {
      werror("Unrecognized option "+tmp+".\n");
      exit(1);
    }
  }

  switch(target)
  {
    case "exe":
      if(!output) output="a.out";
      cflags+=({"-Fe"+fixpath(output)+".exe","BINMODE.OBJ"});
//      cflags+=({"-MT" + (debug?"d":"") });
      wantfile=output+".exe";
      break;

    case "obj":
      if(!output)
	output=basename(remove_ext(argv[1])+".o");
      cflags+=({"-Fo"+fixpath(output+"bj")});
      wantfile=output;
      break;

    case "asm":
      if(!output)
        output=basename(remove_ext(argv[1])+".s");
      cflags+=({"-Fa"+fixpath(output)});
      wantfile=output;
      break;

    case "dll":
      if(output)
	cflags+=({
#ifndef OLD_VC
	  "-LD",
#endif
	  "-Fe"+fixpath(output),
	});
      else
	output=remove_ext(argv[1])+".dll";

#if defined (OLD_VC) && !defined (WIN64)
      cflags+=({"-MD" + (/*debug?"d":*/""), "-LD" + (/*debug?"d":*/"")});
#endif /* !WIN64 */
      wantfile=output;
      break;

    case "-":
  }

  if (!crt_lib) {
    cflags+=({"-MT"});
    crt_lib = "libcmt.lib";
  }

  if(output) rm(output);

  foreach (libs; int i; string lib) libs[i] = fixpath (lib);
  array(string) cmd = ({ compiler }) + cflags + libs;

  switch(target)
  {
    case "exe":
    case "dll":
      // This is necessary mainly to make configure tests reliable: It
      // seems that the linker looks at the timestamp to decide
      // whether an object file has been updated when linking
      // incrementally. Sometimes the conftest programs are replaced
      // and compiled within one second, in which case the timestamps
      // might remain the same if the filesystem is mounted from a
      // Samba server.
      //
      // Besides, in VC8 I can't get incremental linking to work
      // anyway for pike.exe, where it'd be really useful. /mast
      ldopts += ({"-incremental:no"});

      if(debug)
      {
#ifdef OLD_VC
	ldopts = ({ 
	  "-fixed:no",
	  "-debug",
//	  "-PDB:NONE",
	  "-DEBUGTYPE:BOTH",
	  "-DEBUG",
	}) + ldopts;
#else
	ldopts += ({"-debug"});
#endif
	if(dynamic)
	  // The -debugtype flag doesn't work with VC8.
	  ldopts += ({"-DEBUGTYPE:BOTH"});
      }
      else if(dynamic)
      {
	// The -debugtype flag doesn't work with VC8.
	ldopts = ({ "-DEBUG", "-DEBUGTYPE:COFF"}) + ldopts;
      }

      // Libs to ignore. C.f.
      // http://msdn.microsoft.com/en-us/library/aa267384.aspx
      ldopts += map (({"libc.lib", "libcmt.lib",/* "msvcrt.lib",*/ 
		       "libcd.lib", "libcmtd.lib", "msvcrtd.lib",
		     }) - ({crt_lib}),
		     lambda (string lib) {
		       return "-nodefaultlib:" + lib;
		     });
      break;

    default:
      break;
  }

  if(sizeof(ldopts))
    cmd += ({"-link" }) + ldopts;

  //werror ("%O\n", cmd);

  array(string) removeflags = ({});
  switch (getenv ("RNTCL_VC")) {
    case "6": removeflags = removeflags_vc6; break;
    case "7": removeflags = removeflags_vc7; break;
    case "8": removeflags = removeflags_vc8; break;
    case "9": removeflags = removeflags_vc9; break;
    case 0: break;
    default:
      werror ("Warning: Unknown RNTCL_VC compat level %O.\n",
	      getenv ("RNTCL_VC"));
      break;
  }
  foreach (removeflags, string flag_glob)
    cmd -= glob (flag_glob, cmd);

  int ret;
  if(verbose && target!="-")
  { 
    werror("   %s\n",cmd*" ");
    ret=do_cmd(cmd, 0, 1);
  }else{
    int first_line=1;
    int forced_error=0;
    array(string) trailer = ({"", "", ""});
    // cl.exe and ecl.exe echo the file name of the file we are compiling
    // first, we need to get rid of that to make configure behave.
    // CL: We also convert warnings D4002 and D9002 (both are "ignoring
    //     unknown option") into errors.
    // ECL: We also convert warning 147 into an error, and command line
    //      remark "option not supported" into an error.
    ret=silent_do_cmd(cmd, lambda(string line, void|int channel)
		      {
			trailer[channel] += line;
			array(string) lines = trailer[channel]/"\n";
			if (sizeof(lines) < 2) return;
			if(first_line && channel != 2)
			{
			  // Strip the first line if it is the name of the
			  // file being compiled.
			  if (lines[0] == compile_source) {
#if 0
			    // Always false due to test above..
			    if (verbose && (target != "-"))
			      write(lines[0]+"\n");
#endif
			    lines = lines[1..];
			  }
			  first_line = 0;
			}
			trailer[channel] = lines[-1];
			lines = lines[..sizeof(lines)-2];
			for(int i; i < sizeof(lines); i++) {
			  // FIXME: The following checks should
			  // probably only check the stderr (2) or
			  // unknown (0) channels.
			  //
			  // Better not, since cl.exe is known to happily send
			  // errors/warnings to either stdout and stderr.. :P
			  // /mast
#ifdef ECL
			  // Warning 147 is prototype mismatch.
			  // for some reason this isn't an error.
			  // We make it an error...
			  if (search(lines[i], "warning #147:") != -1) {
			    forced_error = 1;
			    lines[i] += " (rntcl: converted to error)";
			  } else {
			    // Make it possible to test if flags are supported.
			    if (search(lines[i],
				       "Command line remark: option '") != -1){
			      if (search(lines[i], "' not supported") != -1) {
				forced_error = 1;
				lines[i] += " (rntcl: converted to error)";
			      }
			    }
			  }
#else /* !ECL */
			  foreach (({"D4002", "D9002"}), string warn_num) {
			    if (has_value (lines[i], "Command line warning " +
					   warn_num)) {
			      forced_error=1;
			      lines[i] += " (rntcl: converted to error)";
			    }
			  }
#endif /* ECL */
			}
			if (sizeof(lines)) {
			  if (channel == 2) werror (lines * "\n" + "\n");
			  else write(lines*"\n" + "\n");
			}
		      }, 1, 1);
    if (sizeof(trailer[0])) {
      // Shouldn't happen, but...
      write(trailer[0]);
    }
    if (sizeof(trailer[1])) write(trailer[1]);
    if (sizeof(trailer[2])) werror(trailer[2]);
    ret = ret || forced_error;
  }

  if(ret)
  {
    werror("CL returned error code %d.\n",ret);
    exit(ret);
  }

  if(wantfile)
  {
    if (target == "obj")
      safe_hardlink (wantfile + "bj", wantfile);
    else if(!paranoia_stat(wantfile))
    {
      werror("RNTCL: output file not generated (%s in %s).\n",wantfile, getcwd());
      exit(1);
    }
  }

  if(target=="exe" && !share)
  {
    rm(output);
    Stdio.write_file(output,
		     "#!/usr/bin/env pike\n"
		     "inherit \""+find_lib_location()+"\";\n"
		     "int main(int argc, array(string) argv) {\n"
		     "  if (lower_case(getenv(\"CROSSCOMPILING\")||\"no\") != \"no\")\n"
		     "    exit(1);\n"
		     "  argv[0]=fixpath(combine_path(getcwd(),argv[0])+\".exe\");\n"
		     "  exit(silent_do_cmd(argv));\n"
		     "}\n");
    chmod(output,0755);
  }

  exit(ret);
}
