instantfptools.pas 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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. Findclose(FileInfo);
  242. end;
  243. if not RemoveDir(Directory) then begin
  244. writeln('unable to delete directory "'+Directory+'"');
  245. Halt(1);
  246. end;
  247. end;
  248. procedure Compile(const SrcFilename, CacheFilename, OutputFilename: string);
  249. var
  250. Compiler: String;
  251. CompParams: String;
  252. Proc: TProcess;
  253. Count: Int64;
  254. ss: TStringStream;
  255. buf : Array[1..4096] of byte;
  256. pid: SizeUInt;
  257. BuildDir: String;
  258. OutputFilenameExe, BuildOutputFilename: String;
  259. procedure CleanUp;
  260. begin
  261. if BuildDir<>'' then begin
  262. // delete build directory
  263. DeleteDirectory(BuildDir);
  264. end;
  265. end;
  266. begin
  267. Compiler:=GetCompiler;
  268. pid:=GetProcessID;
  269. BuildDir:='';
  270. OutputFilenameExe:=OutputFilename {$IFDEF HASEXEEXT} + '.exe' {$ENDIF};
  271. BuildOutputFilename:=OutputFilenameExe;
  272. if pid>0 then begin
  273. BuildDir:=ExtractFilePath(OutputFilenameExe)+'__tmp'+IntToStr(pid)+PathDelim;
  274. BuildOutputFilename:=BuildDir+ExtractFileName(OutputFilenameExe);
  275. end;
  276. //writeln('Compiler=',Compiler,' Params=',CompParams);
  277. if FileExists(OutputFilenameExe) and not DeleteFile(OutputFilenameExe) then begin
  278. writeln('unable to delete ',OutputFilenameExe);
  279. Halt(1);
  280. end;
  281. if BuildDir<>'' then begin
  282. if FileExists(BuildOutputFilename) and not DeleteFile(BuildOutputFilename)
  283. then begin
  284. writeln('unable to delete ',BuildOutputFilename);
  285. Halt(1);
  286. end;
  287. if not DirectoryExists(BuildDir) and not CreateDir(BuildDir) then begin
  288. writeln('unable to mkdir ',BuildDir);
  289. Halt(1);
  290. end;
  291. end;
  292. try
  293. CompParams:=GetCompilerParameters(CacheFilename,BuildDir,BuildOutputFilename);
  294. Proc:=TProcess.Create(nil);
  295. Proc.CommandLine:=Compiler+' '+CompParams;
  296. {$WARNING Unconditional use of pipes breaks for targets not supporting them}
  297. Proc.Options:= [poUsePipes, poStdErrToOutput];
  298. Proc.ShowWindow := swoHide;
  299. Proc.Execute;
  300. ss:=TStringStream.Create('');
  301. repeat
  302. Count:=Proc.Output.Read(Buf{%H-},4096);
  303. if Count>0 then
  304. ss.write(buf,count);
  305. until Count=0;
  306. if (not Proc.WaitOnExit) or (Proc.ExitStatus<>0) then begin
  307. WriteCompilerOutput(SrcFilename,BuildOutputFilename,ss.DataString);
  308. CleanUp;
  309. Halt(1);
  310. end;
  311. if BuildDir<>'' then begin
  312. // move from build directory to cache
  313. if not RenameFile(BuildOutputFilename,OutputFilenameExe) then begin
  314. writeln('unable to move "',BuildOutputFilename,'" to "',OutputFilenameExe,'"');
  315. Halt(1);
  316. end;
  317. end;
  318. ss.Free;
  319. Proc.Free;
  320. finally
  321. CleanUp;
  322. end;
  323. end;
  324. function GetCompilerParameters(const SrcFilename, OutputDirectory,
  325. OutputFilename: string): string;
  326. { For example:
  327. /usr/bin/instantfpc -MObjFpc -Sh ./envvars.pas param1
  328. The shebang compile parameters: -MObjFpc -Sh
  329. }
  330. procedure InterpretParam(p : String);
  331. begin
  332. if (Copy(p,1,1)='-') and (copy(p,1,2)<>'--') then
  333. AddParam(P,Result);
  334. end;
  335. var
  336. p: String;
  337. i,j : integer;
  338. begin
  339. Result:=GetEnvironmentVariable('INSTANTFPCOPTIONS');
  340. I:=1;
  341. While (I<=ParamCount) and (Copy(ParamStr(i),1,1)='-') do
  342. begin
  343. p:=ParamStr(i);
  344. if (I<>1) then
  345. begin
  346. InterpretParam(p);
  347. end
  348. else
  349. begin
  350. // The linux kernel passes all arguments in the shebang line as 1 argument.
  351. // We must parse and split it ourselves.
  352. Repeat
  353. J:=Pos(' ',P);
  354. if (J=0) then
  355. J:=Length(P)+1;
  356. InterpretParam(Copy(P,1,J-1));
  357. Delete(P,1,J);
  358. Until (P='');
  359. end;
  360. inc(I);
  361. end;
  362. if OutputDirectory<>'' then
  363. AddParam('-FU'+OutputDirectory,Result);
  364. AddParam('-o'+OutputFilename,Result);
  365. AddParam(SrcFilename,Result);
  366. end;
  367. procedure Run(const Filename: string);
  368. var
  369. p : PPChar;
  370. {$IFNDEF UseFpExecV}
  371. i : integer;
  372. args : array of string;
  373. {$ENDIF}
  374. begin
  375. p:=argv;
  376. inc(p);
  377. while (p<>nil) do begin
  378. if (p^<>nil) and (p^^<>'-') then begin
  379. break;
  380. end;
  381. inc(p);
  382. end;
  383. {$IFDEF UseFpExecV}
  384. Halt(FpExecV(Filename,p));
  385. {$ELSE}
  386. if paramcount>1 then
  387. begin
  388. setlength(args,paramcount-1);
  389. for i:=2 to paramcount do
  390. args[i-2]:=paramstr(i);
  391. end;
  392. Halt(ExecuteProcess(Filename,args));
  393. {$ENDIF}
  394. end;
  395. end.