{ Command line utility to create closed source Lazarus packages. Run with -h to see help Copyright (C) 2013 Mattias Gaertner mattias@freepascal.org This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version with the following modification: As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules,and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. } program sourcecloser; {$mode objfpc}{$H+} uses Classes, SysUtils, AvgLvlTree, LazLogger, LazFileUtils, Laz2_XMLCfg, LazUTF8, FileProcs, BasicCodeTools, CodeToolManager, CodeCache, SourceChanger, CodeTree, DefineTemplates, CustApp; type { TTarget } TTarget = class public Names, Values: array of string; procedure Add(Name, Value: string); function AsString: string; end; { TSourceCloser } TSourceCloser = class(TCustomApplication) private FClosedSrcError: TStringList; FCompilerOptions: string; FDefines: TStringToStringTree; FDisableCompile: boolean; FIncludePath: string; FLPKFilenames: TStrings; FRemovePrivateSections: boolean; FUndefines: TStringToStringTree; FUnitFilenames: TStrings; FVerbosity: integer; fDefinesApplied: boolean; procedure DeleteNode(Tool: TCodeTool; Node: TCodeTreeNode; const StartPos, EndPos: Integer; Changer: TSourceChangeCache; AddEmptyLine: boolean); protected procedure DoRun; override; procedure ApplyDefines; procedure ConvertLPK(LPKFilename: string); procedure ConvertUnit(UnitFilename: string); public constructor Create(TheOwner: TComponent); override; destructor Destroy; override; procedure WriteHelp; virtual; property Verbosity: integer read FVerbosity write FVerbosity; property Defines: TStringToStringTree read FDefines; property Undefines: TStringToStringTree read FUndefines; property IncludePath: string read FIncludePath write FIncludePath; property LPKFilenames: TStrings read FLPKFilenames; property UnitFilenames: TStrings read FUnitFilenames; property CompilerOptions: string read FCompilerOptions write FCompilerOptions; property RemovePrivateSections: boolean read FRemovePrivateSections write FRemovePrivateSections; property ClosedSrcError: TStringList read FClosedSrcError write FClosedSrcError; property DisableCompile: boolean read FDisableCompile write FDisableCompile; end; function IndexOfFilename(List: TStrings; Filename: string): integer; begin Result:=List.Count-1; while (Result>=0) and (CompareFilenames(List[Result],Filename)<>0) do dec(Result); end; { TTarget } procedure TTarget.Add(Name, Value: string); var Cnt: Integer; begin Cnt:=length(Names); SetLength(Names,Cnt+1); Names[Cnt]:=Name; SetLength(Values,Cnt+1); Values[Cnt]:=Value; end; function TTarget.AsString: string; var i: Integer; begin Result:=''; for i:=0 to length(Names)-1 do begin if Result<>'' then Result+=','; Result+=Names[i]+'='+Values[i]; end; end; { TSourceCloser } procedure TSourceCloser.DeleteNode(Tool: TCodeTool; Node: TCodeTreeNode; const StartPos, EndPos: Integer; Changer: TSourceChangeCache; AddEmptyLine: boolean); procedure E(Msg: string); begin writeln('ERROR: '+Msg); Halt(1); end; var EndCodePos: TCodePosition; StartCodePos: TCodePosition; s: String; begin //debugln(['TSourceCloser.DeleteNode ',Node.DescAsString,' "',dbgstr(Tool.Src,StartPos,EndPos-StartPos),'"']); if not Tool.CleanPosToCodePos(StartPos, StartCodePos) then E('unable to delete '+Node.DescAsString+' of "'+Tool.MainFilename+'" (invalid startpos ' +Tool.CleanPosToStr(StartPos, true)+')'); if not Tool.CleanPosToCodePos(EndPos, EndCodePos) then E('unable to delete '+Node.DescAsString+' of "'+Tool.MainFilename+'" (invalid endpos '+ Tool.CleanPosToStr(EndPos, true)+')'); if StartCodePos.Code<>EndCodePos.Code then E('unable to delete '+Node.DescAsString+' of "'+Tool.MainFilename+'" from '+ Tool.CleanPosToStr(StartPos, true)+' to '+Tool.CleanPosToStr(EndPos, true) ); s:=''; if AddEmptyLine then s:=LineEnding; if not Changer.ReplaceEx(gtNone, gtNone, 0, 0, StartCodePos.Code, StartCodePos.P, EndCodePos.P, s) then E('unable to delete '+Node.DescAsString+' of "'+Tool.MainFilename+'"'); end; procedure TSourceCloser.DoRun; const ShortOpts = 'hvqc:pd:u:i:ke:'; LongOpts = 'help verbose quiet compileroptions: disablecompile define: undefine: includepath: keepprivate error:'; var ErrMsgIsDefault: Boolean; procedure E(Msg: string; WithHelp: boolean = false); begin DebugLn(['ERROR: ',Msg]); if WithHelp then WriteHelp; Terminate; Halt(1); end; procedure ParseValueParam(ShortOpt: char; Value: string); begin case ShortOpt of 'c': begin FCompilerOptions+=Value; end; 'e': begin if ErrMsgIsDefault then begin ErrMsgIsDefault:=false; FClosedSrcError.Clear; end; Value:=UTF8Trim(Value,[]); if Value='' then exit; FClosedSrcError.Add(Value); end; 'i': begin Value:=UTF8Trim(Value,[]); if Value='' then exit; if IncludePath<>'' then Value:=';'+Value; fIncludePath+=Value; end; 'd': begin if not IsValidIdent(Value) then E('invalid define:'+Value); Defines[Value]:=''; end; 'u': begin if not IsValidIdent(Value) then E('invalid define:'+Value); Defines[Value]:=''; end; else E('invalid option "'+ShortOpt+'"'); end; end; procedure AddFile(aFilename: string); var Ext: String; begin debugln(['AddFile ',aFilename]); Ext:=lowercase(ExtractFileExt(aFilename)); if Ext='.lpk' then begin if IndexOfFilename(LPKFilenames,aFilename)>=0 then E('duplicate lpk:'+aFilename); // duplicate lpk, compilation order is unclear => error LPKFilenames.Add(aFilename); end else if FilenameIsPascalUnit(aFilename) then begin if IndexOfFilename(UnitFilenames,aFilename)>=0 then exit; // duplicate unit is ok => ignore UnitFilenames.Add(aFilename); end else E('only lpk and units are supported, invalid file:'+aFilename); end; procedure AddFiles(Pattern: string); var Info: TSearchRec; begin if FindFirstUTF8(Pattern,faAnyFile,Info)=0 then begin repeat if (Info.Name='.') or (Info.Name='..') then continue; if (faDirectory and Info.Attr)>0 then continue; AddFile(ExtractFilePath(Pattern)+Info.Name); until FindNextUTF8(Info)<>0; end; FindCloseUTF8(Info); end; var ErrorMsg: String; i: Integer; Param: String; S2SItem: PStringToStringItem; Filename: String; Option: string; p: SizeInt; begin // quick check parameters ErrorMsg:=CheckOptions(ShortOpts,LongOpts); if ErrorMsg<>'' then E(ErrorMsg); // parse parameters if HasOption('h','help') then begin WriteHelp; Terminate; Exit; end; ErrMsgIsDefault:=true; i:=1; while i<=System.ParamCount do begin Param:=ParamStrUTF8(i); inc(i); if Param='' then continue; if (Param='-q') or (Param='--quiet') then dec(fVerbosity) else if (Param='-v') or (Param='--verbose') then inc(fVerbosity) else if (Param='-k') or (Param='--keepprivate') then RemovePrivateSections:=false else if (Param='-p') or (Param='--disablecompile') then DisableCompile:=true else if Param[1]<>'-' then begin Filename:=TrimAndExpandFilename(Param); if (Pos('*',ExtractFileName(Filename))>0) or (Pos('?',ExtractFileName(Filename))>0) then begin AddFiles(Filename); end else begin if not FileExistsUTF8(Filename) then E('file not found: '+Param); if DirPathExists(Filename) then E('file is a directory: '+Param); AddFile(Filename); end; end else if (length(Param)=2) and (Pos(Param[2]+':',ShortOpts)>0) then begin // e.g. -t Option:=Param[2]; if i>System.ParamCount then E('missing value for option: '+Param); Param:=ParamStrUTF8(i); inc(i); ParseValueParam(Option[1],Param); end else if (copy(Param,1,2)='--') then begin p:=Pos('=',Param); if p<1 then E('invalid long option syntax: '+Param); Option:=copy(Param,3,p-3); delete(Param,1,p); if Option='compileroptions' then Option:='c' else if Option='define' then Option:='d' else if Option='undefine' then Option:='u' else if Option='includepath' then Option:='i' else if Option='error' then Option:='e' else E('invalid long option'); ParseValueParam(Option[1],Param); end else E('invalid option: '+Param); end; if Verbosity>0 then begin debugln(['Options:']); debugln(['Verbosity=',Verbosity]); debugln(['IncludePath=',IncludePath]); for S2SItem in Defines do debugln('Define:',S2SItem^.Name); for S2SItem in Undefines do debugln('Undefine:',S2SItem^.Name); for i:=0 to LPKFilenames.Count-1 do debugln(['LPK[',i+1,']:',LPKFilenames[i]]); for i:=0 to UnitFilenames.Count-1 do debugln(['Unit[',i+1,']:',UnitFilenames[i]]); end; if (LPKFilenames.Count=0) and (UnitFilenames.Count=0) then E('you must pass at least one lpk or pas file',true); for i:=0 to LPKFilenames.Count-1 do ConvertLPK(LPKFilenames[i]); for i:=0 to UnitFilenames.Count-1 do ConvertUnit(UnitFilenames[i]); // stop program loop Terminate; end; procedure TSourceCloser.ApplyDefines; var IncPathTemplate: TDefineTemplate; S2SItem: PStringToStringItem; MacroName: String; DefTemplate: TDefineTemplate; begin if fDefinesApplied then exit; fDefinesApplied:=true; CodeToolBoss.SimpleInit('codetools.cache'); if IncludePath<>'' then begin IncPathTemplate:=TDefineTemplate.Create('IncPath', 'extending include search path', IncludePathMacroName, // variable name: #IncPath '$('+IncludePathMacroName+');'+IncludePath ,da_DefineRecurse ); CodeToolBoss.DefineTree.Add(IncPathTemplate); end; for S2SItem in Defines do begin MacroName:=S2SItem^.Name; DefTemplate:=TDefineTemplate.Create('Define '+MacroName, 'Define '+MacroName, MacroName,S2SItem^.Value,da_DefineRecurse); CodeToolBoss.DefineTree.Add(DefTemplate); end; for S2SItem in Undefines do begin MacroName:=S2SItem^.Name; DefTemplate:=TDefineTemplate.Create('Undefine '+MacroName, 'Undefine '+MacroName, MacroName,'',da_UndefineRecurse); CodeToolBoss.DefineTree.Add(DefTemplate); end; end; procedure TSourceCloser.ConvertLPK(LPKFilename: string); // set lpk to compile only manually // add -Ur to compiler options const OtherPath='Package/CompilerOptions/Other/'; CustomOptionsPath=OtherPath+'CustomOptions/Value'; var xml: TXMLConfig; CustomOptions: String; NewOptions: String; begin debugln(['Converting lpk: ',LPKFilename]); xml:=TXMLConfig.Create(LPKFilename); try // set lpk to compile only manually xml.SetValue('Package/AutoUpdate/Value','Manually'); // add -Ur to compiler options CustomOptions:=xml.GetValue(CustomOptionsPath,''); NewOptions:=CustomOptions; if Pos('-Ur',NewOptions)<1 then begin if NewOptions<>'' then NewOptions+=' '; NewOptions+='-Ur'; end; if FCompilerOptions<>'' then begin if NewOptions<>'' then NewOptions+=' '; NewOptions+=FCompilerOptions; end; xml.SetValue(CustomOptionsPath,NewOptions); // disable compile commands if DisableCompile then begin xml.SetValue(OtherPath+'CompilerPath/Value',''); xml.SetDeleteValue(OtherPath+'ExecuteBefore/Command/Value','',''); xml.SetDeleteValue(OtherPath+'ExecuteAfter/Command/Value','',''); xml.SetDeleteValue(OtherPath+'CreateMakefileOnBuild/Value','',''); end; // write xml.Flush; finally xml.Free; end; end; procedure TSourceCloser.ConvertUnit(UnitFilename: string); procedure E(Msg: string); begin writeln('ERROR: '+Msg); Halt(1); end; var Code: TCodeBuffer; Changer: TSourceChangeCache; Tool: TCodeTool; Node: TCodeTreeNode; StartPos: Integer; EndPos: Integer; CodeList: TFPList; i: Integer; FromPos: Integer; ToPos: Integer; AddEmptyLine: Boolean; s: String; begin debugln(['Converting unit: ',UnitFilename]); ApplyDefines; // load file Code:=CodeToolBoss.LoadFile(UnitFilename,true,false); if Code=nil then E('unable to read "'+UnitFilename+'"'); // parse whole unit if (not CodeToolBoss.Explore(Code,Tool,false)) or (CodeToolBoss.ErrorMessage<>'') then E('parse error'); if Tool.GetSourceType<>ctnUnit then E('not a unit, skipping "'+Code.Filename+'"'); // init SourceChangeCache Changer:=CodeToolBoss.SourceChangeCache; Changer.MainScanner:=Tool.Scanner; // add errors s:=''; for i:=0 to FClosedSrcError.Count-1 do begin s:=s+'{$Error '+FClosedSrcError[i]+'}'+LineEnding; end; if s<>'' then Changer.ReplaceEx(gtNone,gtNone,0,0,Code,1,1,s); if RemovePrivateSections then begin // delete private sections in the interface Node:=Tool.FindInterfaceNode; while Node<>nil do begin if Node.Desc=ctnClassPrivate then begin FromPos:=Tool.FindLineEndOrCodeInFrontOfPosition(Node.StartPos); ToPos:=Tool.FindLineEndOrCodeInFrontOfPosition(Node.EndPos); DeleteNode(Tool,Node,FromPos,ToPos,Changer,false); Node:=Node.NextSkipChilds; end else Node:=Node.Next; if Node=nil then break; if Node.Desc in [ctnImplementation,ctnInitialization,ctnFinalization] then break; end; end; // delete implementation, initialization and finalization section Node:=Tool.Tree.Root; while (Node<>nil) do begin if Node.Desc=ctnImplementation then begin // delete implementation section including the 'implementation' keyword StartPos:=Node.StartPos; EndPos:=Node.NextBrother.StartPos; DeleteNode(Tool, Node, StartPos, EndPos, Changer,false); end else if Node.Desc in [ctnInitialization,ctnFinalization] then begin // delete the content of the finalization and initialization section Tool.MoveCursorToNodeStart(Node); Tool.ReadNextAtom; // read 'initialization' StartPos:=Tool.CurPos.EndPos; EndPos:=Node.NextBrother.StartPos; AddEmptyLine:=true; if Trim(copy(Tool.Src,StartPos,EndPos-StartPos))='' then begin // empty initialization => delete keyword as well StartPos:=Node.StartPos; AddEmptyLine:=false; end else begin // initialization section is not empty // => keep the keyword, because it is needed by tools like 'Unused units' end; DeleteNode(Tool, Node, StartPos, EndPos, Changer, AddEmptyLine); end; Node:=Node.NextBrother; end; // apply changes and write changes to disk CodeList:=TFPList.Create; try for i:=0 to Changer.BuffersToModifyCount-1 do CodeList.Add(Changer.BuffersToModify[i]); if not Changer.Apply then E('unable to modify "'+UnitFilename+'"'); for i:=0 to CodeList.Count-1 do begin Code:=TCodeBuffer(CodeList[i]); if not Code.Save then E('unable to write "'+Code.Filename+'"'); end; finally CodeList.Free; end; end; constructor TSourceCloser.Create(TheOwner: TComponent); begin inherited Create(TheOwner); StopOnException:=True; fDefines:=TStringToStringTree.Create(false); FUndefines:=TStringToStringTree.Create(false); FLPKFilenames:=TStringList.Create; FUnitFilenames:=TStringList.Create; FRemovePrivateSections:=true; FClosedSrcError:=TStringList.Create; FClosedSrcError.Add('This is a closed source unit. You can not compile it, it was already compiled.'); FClosedSrcError.Add('Probably the IDE has cleaned up and you have to unpack the zip again.'); end; destructor TSourceCloser.Destroy; begin FreeAndNil(FClosedSrcError); FreeAndNil(FLPKFilenames); FreeAndNil(FUnitFilenames); FreeAndNil(FDefines); FreeAndNil(FUndefines); inherited Destroy; end; procedure TSourceCloser.WriteHelp; var i: Integer; begin writeln('Usage:'); writeln(' ',ExeName,' -h'); writeln(' ',ExeName,' [options] [unit1.pas unit2.pp ...] [pkg1.lpk pk2.lpk ..]'); writeln; writeln('Description:'); writeln(' This tool helps creating closed source Lazarus packages.'); writeln; writeln(' If the file names contain * or ? it will be used as mask.'); writeln(' If you pass a lpk file, this tool will set "compile manually"'); writeln(' and appends "-Ur" to the compiler options.'); writeln(' You can pass multiple lpk files and they will be edited in this order.'); writeln(' If you pass a .pas or .pp file it will be treated as a pascal unit and'); writeln(' will remove the implementation, initialization, finalization sections.'); writeln; writeln('Options:'); writeln(' -h, --help : This help messages.'); writeln(' -v, --verbose : be more verbose.'); writeln(' -q, --quiet : be more quiet.'); writeln('Package/lpk options:'); writeln(' --compileroptions='); writeln(' Add custom compiler options to lpk.'); writeln(' -p, --disablecompile'); writeln(' Remove all compile commands from lpk.'); writeln('Unit options:'); writeln(' -d , --define= :'); writeln(' Define Free Pascal macro. Can be passed multiple times.'); writeln(' -u , --undefine= :'); writeln(' Undefine Free Pascal macro. Can be passed multiple times.'); writeln(' -i , --includepath= :'); writeln(' Append to include search path. Can be passed multiple times.'); writeln(' -k, --keepprivate'); writeln(' Keep private sections in interface.'); writeln(' -e , --error='); writeln(' Change the message of the error directive added to the units.'); writeln(' You can add this multiple times to add multiple directives.'); writeln(' The default error messages are:'); for i:=0 to FClosedSrcError.Count-1 do writeln(' ',FClosedSrcError[i]); writeln; writeln('Environment variables:'); writeln(' PP path to compiler,'); writeln(' e.g. C:\lazarus\fpc\2.6.2\bin\i386-win32\fpc.exe'); writeln(' The compiler is queried for the current defines for the'); writeln(' target platform.'); writeln(' FPCTARGET target os, e.g. win32'); writeln(' FPCTARGETCPU target cpu, e.g. i386'); end; var Application: TSourceCloser; begin Application:=TSourceCloser.Create(nil); Application.Title:='SourceCloser'; Application.Run; Application.Free; end.