instantfptools.pas 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. unit InstantFPTools;
  2. {$mode objfpc}{$H+}
  3. {$define UseFpExecV}
  4. {$ifdef WINDOWS}
  5. {$undef UseFpExecV}
  6. {$define HASEXEEXT}
  7. {$endif WINDOWS}
  8. {$ifdef go32v2}
  9. {$undef UseFpExecV}
  10. {$define HASEXEEXT}
  11. {$endif go32v2}
  12. {$ifdef watcom}
  13. {$undef UseFpExecV}
  14. {$define HASEXEEXT}
  15. {$endif watcom}
  16. {$ifdef os2}
  17. {$undef UseFpExecV}
  18. {$define HASEXEEXT}
  19. {$endif go32v2}
  20. {$IFNDEF VER2_4}
  21. {$DEFINE UseExeSearch}
  22. {$ENDIF}
  23. {$if defined(Windows) or defined(darwin) or defined(os2) or defined(go32v2) or defined(watcom)}
  24. {$define CaseInsensitiveFilenames}
  25. {$endif}
  26. interface
  27. uses
  28. {$IFDEF UseFpExecV}
  29. Unix,
  30. {$ENDIF}
  31. Classes, SysUtils, Process;
  32. procedure CheckSourceName(const Filename: string);
  33. procedure CommentShebang(Src: TStringList);
  34. function GetCacheDir: string;
  35. procedure SetCacheDir(AValue : string);
  36. function IsCacheValid(Src: TStringList;
  37. const CachedSrcFile, CachedExeFile: string): boolean;
  38. procedure Compile(const SrcFilename, CacheFilename, OutputFilename: string);
  39. procedure WriteCompilerOutput(SrcFilename, CacheFilename, CompilerOutput: string);
  40. function GetCompiler: string;
  41. procedure SetCompiler(AValue : string);
  42. function GetCompilerParameters(const SrcFilename, OutputDirectory, OutputFilename: string): string;
  43. procedure Run(const Filename: string);
  44. implementation
  45. Var
  46. CmdCacheDir : String;
  47. CmdCompiler : String;
  48. procedure AddParam(p: string; var Line: string);
  49. begin
  50. if p='' then exit;
  51. if Line<>'' then Line:=Line+' ';
  52. if (p[1]<>'"') and (System.Pos(' ',p)>0) then
  53. p:='"'+p+'"';
  54. Line:=Line+p;
  55. end;
  56. procedure CheckSourceName(const Filename: string);
  57. var
  58. Ext: String;
  59. begin
  60. // avoid name clashes
  61. Ext:=lowercase(ExtractFileExt(Filename));
  62. if (Ext<>'') and (Ext<>'.pas') and (Ext<>'.pp') and (Ext<>'.p')
  63. and (Ext<>'.lpr') and (Ext<>'.txt') and (Ext<>'.sh') and (Ext<>'.cgi')
  64. then begin
  65. writeln('invalid source extension ',Ext);
  66. Halt(1);
  67. end;
  68. end;
  69. procedure CommentShebang(Src: TStringList);
  70. var
  71. Line: string;
  72. i: Integer;
  73. begin
  74. // comment shebang #!
  75. if (Src.Count=0) then exit;
  76. Line:=Src[0];
  77. i:=1;
  78. if copy(Line,1,3)=#$EF#$BB#$BF then
  79. inc(i,3);// UTF8 BOM
  80. if (i>length(Line)) or (Line[i]<>'#') then exit;
  81. Src[0]:=copy(Line,1,i-1)+'//'+copy(Line,i,length(Line));
  82. end;
  83. procedure SetCacheDir(AValue : string);
  84. begin
  85. CmdCacheDir:=AValue;
  86. end;
  87. function GetCacheDir: string;
  88. begin
  89. Result:=CmdCacheDir;
  90. if (Result='') then
  91. begin
  92. Result:=GetEnvironmentVariable('INSTANTFPCCACHE');
  93. if Result='' then
  94. begin
  95. Result:=GetEnvironmentVariable('HOME');
  96. {$ifdef WINDOWS}
  97. if Result='' then
  98. Result:=GetEnvironmentVariable('LOCALAPPDATA');
  99. {$endif WINDOWS}
  100. if Result<>'' then
  101. Result:=IncludeTrailingPathDelimiter(Result)+'.cache'+PathDelim+'instantfpc';
  102. end;
  103. end;
  104. if Result='' then begin
  105. writeln('missing environment variable: HOME or INSTANTFPCCACHE or LOCALAPPDATA');
  106. Halt(1);
  107. end;
  108. Result:=IncludeTrailingPathDelimiter(ExpandFileName(Result));
  109. if not ForceDirectories(Result) then begin
  110. writeln('unable to create cache directory "'+Result+'"');
  111. Halt(1);
  112. end;
  113. end;
  114. function IsCacheValid(Src: TStringList; const CachedSrcFile,
  115. CachedExeFile: string): boolean;
  116. var
  117. OldSrc: TStringList;
  118. i: Integer;
  119. p: String;
  120. begin
  121. Result:=false;
  122. for i:=1 to Paramcount do begin
  123. p:=ParamStr(i);
  124. if (p='') or (p[1]<>'-') then break;
  125. if p='-B' then exit; // always compile
  126. end;
  127. if not FileExists(CachedSrcFile) then exit;
  128. if not FileExists(CachedExeFile) then exit;
  129. OldSrc:=TStringList.Create;
  130. OldSrc.LoadFromFile(CachedSrcFile);
  131. Result:=Src.Equals(OldSrc);
  132. {$IFDEF IFFreeMem}
  133. OldSrc.Free;
  134. {$ENDIF}
  135. end;
  136. procedure SetCompiler(AValue : string);
  137. begin
  138. CmdCompiler:=AValue;
  139. end;
  140. procedure WriteCompilerOutput(SrcFilename, CacheFilename, CompilerOutput: string);
  141. var
  142. Lines: TStringList;
  143. i: Integer;
  144. Line: String;
  145. p: SizeInt;
  146. begin
  147. // replace in compiler output CacheFilename with SrcFilename
  148. Lines:=TStringList.Create;
  149. Lines.Text:=CompilerOutput;
  150. {$IFDEF CaseInsensitiveFilenames}
  151. CacheFilename:=LowerCase(CacheFilename);
  152. {$ENDIF}
  153. for i:=0 to Lines.Count-1 do begin
  154. repeat
  155. Line:=Lines[i];
  156. {$IFDEF CaseInsensitiveFilenames}
  157. Line:=LowerCase(Line);
  158. {$ENDIF}
  159. p:=Pos(CacheFilename,Line);
  160. if p<1 then break;
  161. {$IFDEF CaseInsensitiveFilenames}
  162. Line:=Lines[i];
  163. {$ENDIF}
  164. Lines[i]:=copy(Line,1,p-1)+SrcFilename+copy(Line,p+length(CacheFilename),length(Line));
  165. until false;
  166. end;
  167. // write to stdout
  168. writeln(Lines.Text);
  169. {$IFDEF IFFreeMem}
  170. Lines.Free;
  171. {$ENDIF}
  172. end;
  173. function GetCompiler: string;
  174. var
  175. CompFile: String;
  176. {$IFNDEF UseExeSearch}
  177. Path: String;
  178. p: Integer;
  179. StartPos: LongInt;
  180. Dir: String;
  181. {$ENDIF}
  182. begin
  183. Result:=CmdCompiler;
  184. if (Result<>'') then
  185. begin
  186. Result:=ExpandFileName(Result);
  187. if not FileExists(Result) then
  188. begin
  189. writeln('Error: '+Result+' not found, check the --compiler parameter.');
  190. Halt(1);
  191. end;
  192. exit;
  193. end;
  194. {$IFDEF HASEXEEXT}
  195. CompFile:='fpc.exe';
  196. {$ELSE}
  197. CompFile:='fpc';
  198. {$ENDIF}
  199. {$IFDEF UseExeSearch}
  200. Result:=ExeSearch(CompFile);
  201. {$ELSE}
  202. Path:=GetEnvironmentVariable('PATH');
  203. if Path<>'' then begin
  204. p:=1;
  205. while p<=length(Path) do begin
  206. StartPos:=p;
  207. while (p<=length(Path)) and (Path[p]<>':') do inc(p);
  208. if StartPos<p then begin
  209. Dir:=copy(Path,StartPos,p-StartPos);
  210. Result:=ExpandFileName(IncludeTrailingPathDelimiter(Dir))+CompFile;
  211. if FileExists(Result) then exit;
  212. end;
  213. inc(p);
  214. end;
  215. end;
  216. {$ENDIF}
  217. if (Result='') then
  218. begin
  219. writeln('Error: '+CompFile+' not found in PATH');
  220. Halt(1);
  221. end;
  222. end;
  223. procedure DeleteDirectory(Directory: string);
  224. var
  225. FileInfo: TSearchRec;
  226. aFilename: String;
  227. begin
  228. Directory:=ExcludeTrailingPathDelimiter(Directory);
  229. if not DirectoryExists(Directory) then exit;
  230. if FindFirst(Directory+PathDelim+AllFilesMask,faAnyFile,FileInfo)=0 then begin
  231. repeat
  232. if (FileInfo.Name='.') or (FileInfo.Name='..') then continue;
  233. aFilename:=Directory+PathDelim+FileInfo.Name;
  234. if (FileInfo.Attr and faDirectory)>0 then
  235. DeleteDirectory(aFilename)
  236. else if not DeleteFile(aFilename) then begin
  237. writeln('unable to delete file "'+aFilename+'"');
  238. Halt(1);
  239. end;
  240. until FindNext(FileInfo)<>0;
  241. end;
  242. if not RemoveDir(Directory) then begin
  243. writeln('unable to delete directory "'+Directory+'"');
  244. Halt(1);
  245. end;
  246. end;
  247. procedure Compile(const SrcFilename, CacheFilename, OutputFilename: string);
  248. var
  249. Compiler: String;
  250. CompParams: String;
  251. Proc: TProcess;
  252. Count: Int64;
  253. ss: TStringStream;
  254. buf : Array[1..4096] of byte;
  255. pid: SizeUInt;
  256. BuildDir: String;
  257. BuildOutputFilename: String;
  258. procedure CleanUp;
  259. begin
  260. if BuildDir<>'' then begin
  261. // delete build directory
  262. DeleteDirectory(BuildDir);
  263. end;
  264. end;
  265. begin
  266. Compiler:=GetCompiler;
  267. pid:=GetProcessID;
  268. BuildDir:='';
  269. BuildOutputFilename:=OutputFilename;
  270. if pid>0 then begin
  271. BuildDir:=ExtractFilePath(OutputFilename)+'__tmp'+IntToStr(pid)+PathDelim;
  272. BuildOutputFilename:=BuildDir+ExtractFileName(OutputFilename);
  273. end;
  274. //writeln('Compiler=',Compiler,' Params=',CompParams);
  275. if FileExists(OutputFilename) and not DeleteFile(OutputFilename) then begin
  276. writeln('unable to delete ',OutputFilename);
  277. Halt(1);
  278. end;
  279. if BuildDir<>'' then begin
  280. if FileExists(BuildOutputFilename) and not DeleteFile(BuildOutputFilename)
  281. then begin
  282. writeln('unable to delete ',BuildOutputFilename);
  283. Halt(1);
  284. end;
  285. if not DirectoryExists(BuildDir) and not CreateDir(BuildDir) then begin
  286. writeln('unable to mkdir ',BuildDir);
  287. Halt(1);
  288. end;
  289. end;
  290. try
  291. CompParams:=GetCompilerParameters(CacheFilename,BuildDir,BuildOutputFilename);
  292. Proc:=TProcess.Create(nil);
  293. Proc.CommandLine:=Compiler+' '+CompParams;
  294. {$WARNING Unconditional use of pipes breaks for targets not supporting them}
  295. Proc.Options:= [poUsePipes, poStdErrToOutput];
  296. Proc.ShowWindow := swoHide;
  297. Proc.Execute;
  298. ss:=TStringStream.Create('');
  299. repeat
  300. Count:=Proc.Output.Read(Buf{%H-},4096);
  301. if Count>0 then
  302. ss.write(buf,count);
  303. until Count=0;
  304. if (not Proc.WaitOnExit) or (Proc.ExitStatus<>0) then begin
  305. WriteCompilerOutput(SrcFilename,BuildOutputFilename,ss.DataString);
  306. CleanUp;
  307. Halt(1);
  308. end;
  309. if BuildDir<>'' then begin
  310. // move from build directory to cache
  311. if not RenameFile(BuildOutputFilename,OutputFilename) then begin
  312. writeln('unable to move "',BuildOutputFilename,'" to "',OutputFilename,'"');
  313. Halt(1);
  314. end;
  315. end;
  316. ss.Free;
  317. Proc.Free;
  318. finally
  319. CleanUp;
  320. end;
  321. end;
  322. function GetCompilerParameters(const SrcFilename, OutputDirectory,
  323. OutputFilename: string): string;
  324. { For example:
  325. /usr/bin/instantfpc -MObjFpc -Sh ./envvars.pas param1
  326. The shebang compile parameters: -MObjFpc -Sh
  327. }
  328. var
  329. p: String;
  330. i : integer;
  331. begin
  332. Result:=GetEnvironmentVariable('INSTANTFPCOPTIONS');
  333. I:=1;
  334. While (I<=ParamCount) and (Copy(ParamStr(i),1,1)='-') do
  335. begin
  336. p:=ParamStr(i);
  337. if (Copy(p,1,1)='-') and (copy(p,1,2)<>'--') then
  338. AddParam(P,Result);
  339. inc(I);
  340. end;
  341. if OutputDirectory<>'' then
  342. AddParam('-FU'+OutputDirectory,Result);
  343. AddParam('-o'+OutputFilename {$IFDEF HASEXEEXT} + '.exe' {$ENDIF},Result);
  344. AddParam(SrcFilename,Result);
  345. end;
  346. procedure Run(const Filename: string);
  347. var
  348. p : PPChar;
  349. {$IFNDEF UseFpExecV}
  350. i : integer;
  351. args : array of string;
  352. {$ENDIF}
  353. begin
  354. p:=argv;
  355. inc(p);
  356. while (p<>nil) do begin
  357. if (p^<>nil) and (p^^<>'-') then begin
  358. break;
  359. end;
  360. inc(p);
  361. end;
  362. {$IFDEF UseFpExecV}
  363. Halt(FpExecV(Filename,p));
  364. {$ELSE}
  365. if paramcount>1 then
  366. begin
  367. setlength(args,paramcount-1);
  368. for i:=2 to paramcount do
  369. args[i-2]:=paramstr(i);
  370. end;
  371. Halt(ExecuteProcess(Filename,args));
  372. {$ENDIF}
  373. end;
  374. end.