dexed/src/ce_dubproject.pas

787 lines
21 KiB
Plaintext

unit ce_dubproject;
{$I ce_defines.inc}
interface
uses
Classes, SysUtils, fpjson, jsonparser, jsonscanner, process, strutils,
ce_common, ce_interfaces, ce_observer, ce_dialogs;
type
TCEDubProject = class(TComponent, ICECommonProject)
private
fPackageName: string;
fFilename: string;
fModified: boolean;
fJSON: TJSONObject;
fSrcs: TStringList;
fProjectSubject: TCEProjectSubject;
fConfigsCount: integer;
fImportPaths: TStringList;
fBuildTypes: TStringList;
fConfigs: TStringList;
fBuiltTypeIx: integer;
fConfigIx: integer;
fBinKind: TProjectBinaryKind;
fBasePath: string;
fModificationCount: integer;
fOutputFileName: string;
//
procedure doModified;
procedure updateFields;
procedure updatePackageNameFromJson;
procedure udpateConfigsFromJson;
procedure updateSourcesFromJson;
procedure updateTargetKindFromJson;
procedure updateImportPathsFromJson;
procedure updateOutputNameFromJson;
function findTargetKindInd(value: TJSONObject): boolean;
procedure dubProcOutput(proc: TProcess);
function getCurrentCustomConfig: TJSONObject;
function compileOrRun(run: boolean; const runArgs: string = ''): boolean;
public
constructor create(aOwner: TComponent); override;
destructor destroy; override;
//
procedure beginModification;
procedure endModification;
//
function filename: string;
function basePath: string;
procedure loadFromFile(const aFilename: string);
procedure saveToFile(const aFilename: string);
//
function getFormat: TCEProjectFormat;
function getProject: TObject;
function modified: boolean;
function binaryKind: TProjectBinaryKind;
function getCommandLine: string;
function outputFilename: string;
//
function isSource(const aFilename: string): boolean;
function sourcesCount: integer;
function sourceRelative(index: integer): string;
function sourceAbsolute(index: integer): string;
function importsPathCount: integer;
function importPath(index: integer): string;
//
function configurationCount: integer;
function getActiveConfigurationIndex: integer;
procedure setActiveConfigurationIndex(index: integer);
function configurationName(index: integer): string;
//
function compile: boolean;
function run(const runArgs: string = ''): boolean;
function targetUpToDate: boolean;
//
property json: TJSONObject read fJSON;
end;
// these 9 built types always exist
TDubBuildType = (plain, debug, release, unittest, docs, ddox, profile, cov, unittestcov);
// returns true if filename is a valid dub project. Only json format is supported.
function isValidDubProject(const filename: string): boolean;
implementation
const
DubBuiltTypeName: array[TDubBuildType] of string = ('plain', 'debug', 'release',
'unittest', 'docs', 'ddox', 'profile', 'cov', 'unittest-cov'
);
DubDefaultConfigName = '(default config)';
{$REGION Standard Comp/Obj -----------------------------------------------------}
constructor TCEDubProject.create(aOwner: TComponent);
begin
inherited;
fJSON := TJSONObject.Create();
fProjectSubject := TCEProjectSubject.Create;
fBuildTypes := TStringList.Create;
fConfigs := TStringList.Create;
fSrcs := TStringList.Create;
fImportPaths := TStringList.Create;
//
subjProjNew(fProjectSubject, self);
subjProjChanged(fProjectSubject, self);
end;
destructor TCEDubProject.destroy;
begin
subjProjClosing(fProjectSubject, self);
fProjectSubject.free;
//
fJSON.Free;
fBuildTypes.Free;
fConfigs.Free;
fSrcs.Free;
fImportPaths.Free;
inherited;
end;
{$ENDREGION --------------------------------------------------------------------}
{$REGION ICECommonProject: project props ---------------------------------------}
function TCEDubProject.getFormat: TCEProjectFormat;
begin
exit(pfDub);
end;
function TCEDubProject.getProject: TObject;
begin
exit(self);
end;
function TCEDubProject.modified: boolean;
begin
exit(fModified);
end;
function TCEDubProject.filename: string;
begin
exit(fFilename);
end;
function TCEDubProject.basePath: string;
begin
exit(fBasePath);
end;
procedure TCEDubProject.loadFromFile(const aFilename: string);
var
loader: TMemoryStream;
parser : TJSONParser;
bom: dword = 0;
begin
loader := TMemoryStream.Create;
try
fBasePath := extractFilePath(aFilename);
fFilename := aFilename;
loader.LoadFromFile(fFilename);
//TODO-cDUB: if loaded as UTF8 it should be saved as well !
// skip BOM, this crashes the parser
loader.Read(bom, 4);
if (bom and $BFBBEF) = $BFBBEF then
loader.Position:= 3
else if (bom = $FFFE0000) or (bom = $FEFF) then
begin
// UCS-4 LE/BE not handled by DUB
loader.clear;
loader.WriteByte(byte('{'));
loader.WriteByte(byte('}'));
loader.Position:= 0;
end
else if ((bom and $FEFF) = $FEFF) or ((bom and $FFFE) = $FFFE) then
begin
// UCS-2 LE/BE not handled by DUB
loader.clear;
loader.WriteByte(byte('{'));
loader.WriteByte(byte('}'));
loader.Position:= 0;
end
else
loader.Position:= 0;
//
FreeAndNil(fJSON);
parser := TJSONParser.Create(loader);
try
try
fJSON := parser.Parse as TJSONObject;
except
if assigned(fJSON) then
FreeAndNil(fJSON);
end;
finally
parser.Free;
end;
finally
loader.Free;
updateFields;
subjProjChanged(fProjectSubject, self);
fModified := false;
end;
end;
procedure TCEDubProject.saveToFile(const aFilename: string);
var
saver: TMemoryStream;
str: string;
begin
saver := TMemoryStream.Create;
try
fFilename := aFilename;
str := fJSON.FormatJSON;
saver.Write(str[1], length(str));
saver.SaveToFile(fFilename);
finally
saver.Free;
fModified := false;
end;
end;
function TCEDubProject.binaryKind: TProjectBinaryKind;
begin
exit(fBinKind);
end;
function TCEDubProject.getCommandLine: string;
var
str: TStringList;
begin
str := TStringList.Create;
try
str.Add('dub' + exeExt);
str.Add('build');
str.Add('--build=' + fBuildTypes.Strings[fBuiltTypeIx]);
if (fConfigs.Count <> 1) and (fConfigs.Strings[0] <> DubDefaultConfigName) then
str.Add('--config=' + fConfigs.Strings[fConfigIx]);
result := str.Text;
finally
str.Free;
end;
end;
function TCEDubProject.outputFilename: string;
begin
exit(fOutputFileName);
end;
{$ENDREGION --------------------------------------------------------------------}
{$REGION ICECommonProject: sources ---------------------------------------------}
function TCEDubProject.isSource(const aFilename: string): boolean;
var
fname: string;
begin
fname := aFilename;
if fileExists(fname) then
fname := ExtractRelativepath(fBasePath, fname);
result := fSrcs.IndexOf(fname) <> -1;
end;
function TCEDubProject.sourcesCount: integer;
begin
exit(fSrcs.Count);
end;
function TCEDubProject.sourceRelative(index: integer): string;
begin
exit(fSrcs.Strings[index]);
end;
function TCEDubProject.sourceAbsolute(index: integer): string;
var
fname: string;
begin
fname := fSrcs.Strings[index];
if FileExists(fname) then
result := fname
else
result := expandFilenameEx(fBasePath, fname);
end;
function TCEDubProject.importsPathCount: integer;
begin
result := fImportPaths.Count;
end;
function TCEDubProject.importPath(index: integer): string;
begin
result := expandFilenameEx(fBasePath, fImportPaths.Strings[index]);
end;
{$ENDREGION --------------------------------------------------------------------}
{$REGION ICECommonProject: configs ---------------------------------------------}
function TCEDubProject.configurationCount: integer;
begin
exit(fConfigsCount);
end;
function TCEDubProject.getActiveConfigurationIndex: integer;
begin
exit(fBuiltTypeIx * fConfigs.Count + fConfigIx);
end;
procedure TCEDubProject.setActiveConfigurationIndex(index: integer);
begin
fBuiltTypeIx := index div fConfigs.Count;
fConfigIx := index mod fConfigs.Count;
doModified;
// DUB does not store an active config
fModified:=false;
end;
function TCEDubProject.configurationName(index: integer): string;
begin
result := fBuildTypes.Strings[index div fConfigs.Count] + ' - ' +
fConfigs.Strings[index mod fConfigs.Count];
end;
{$ENDREGION --------------------------------------------------------------------}
{$REGION ICECommonProject: actions ---------------------------------------------}
procedure TCEDubProject.dubProcOutput(proc: TProcess);
var
lst: TStringList;
str: string;
msgs: ICEMessagesDisplay;
begin
lst := TStringList.Create;
msgs := getMessageDisplay;
try
processOutputToStrings(proc, lst);
for str in lst do
msgs.message(str, self as ICECommonProject, amcProj, amkAuto);
finally
lst.Free;
end;
end;
function TCEDubProject.compileOrRun(run: boolean; const runArgs: string = ''): boolean;
var
dubproc: TProcess;
olddir: string;
prjname: string;
msgs: ICEMessagesDisplay;
begin
result := false;
if not FileExists(fFilename) then
begin
dlgOkInfo('The DUB project must be saved before being compiled or run !');
exit;
end;
msgs := getMessageDisplay;
msgs.clearByData(Self as ICECommonProject);
prjname := shortenPath(fFilename);
dubproc := TProcess.Create(nil);
olddir := GetCurrentDir;
try
if not run then
begin
msgs.message('compiling ' + prjname, self as ICECommonProject, amcProj, amkInf);
if modified then saveToFile(fFilename);
end;
chDir(extractFilePath(fFilename));
dubproc.Executable := 'dub' + exeExt;
dubproc.Options := dubproc.Options + [poStderrToOutPut, poUsePipes];
dubproc.CurrentDirectory := extractFilePath(fFilename);
dubproc.ShowWindow := swoHIDE;
if not run then
dubproc.Parameters.Add('build')
else
dubproc.Parameters.Add('run');
dubproc.Parameters.Add('--build=' + fBuildTypes.Strings[fBuiltTypeIx]);
if (fConfigs.Count <> 1) and (fConfigs.Strings[0] <> DubDefaultConfigName) then
dubproc.Parameters.Add('--config=' + fConfigs.Strings[fConfigIx]);
if run and (runArgs <> '') then
dubproc.Parameters.Add('--' + runArgs);
dubproc.Execute;
while dubproc.Running do
dubProcOutput(dubproc);
if not run then
begin
if dubproc.ExitStatus = 0 then begin
msgs.message(prjname + ' has been successfully compiled', self as ICECommonProject, amcProj, amkInf);
result := true;
end else
msgs.message(prjname + ' has not been compiled', self as ICECommonProject, amcProj, amkWarn);
end;
finally
chDir(olddir);
dubproc.Free;
end;
end;
function TCEDubProject.compile: boolean;
begin
result := compileOrRun(false);
end;
function TCEDubProject.run(const runArgs: string = ''): boolean;
begin
result := compileOrRun(true);
end;
function TCEDubProject.targetUpToDate: boolean;
begin
// rebuilding is done automatically when the command is 'run'
result := true;
end;
{$ENDREGION --------------------------------------------------------------------}
{$REGION JSON to internal fields -----------------------------------------------}
function TCEDubProject.getCurrentCustomConfig: TJSONObject;
var
item: TJSONData;
confs: TJSONArray;
begin
result := nil;
if fConfigIx = 0 then exit;
//
item := fJSON.Find('configurations');
if not assigned(item) then exit;
//
confs := TJSONArray(item);
if fConfigIx > confs.Count -1 then exit;
//
result := confs.Objects[fConfigIx];
end;
procedure TCEDubProject.updatePackageNameFromJson;
var
value: TJSONData;
begin
if not assigned(fJSON) then
exit;
value := fJSON.Find('name');
if not assigned(value) then fPackageName := ''
else fPackageName := value.AsString;
end;
procedure TCEDubProject.udpateConfigsFromJson;
var
i: integer;
dat: TJSONData;
arr: TJSONArray = nil;
item: TJSONObject = nil;
obj: TJSONObject = nil;
itemname: string;
begin
fBuildTypes.Clear;
fConfigs.Clear;
if not assigned(fJSON) then
exit;
// the CE interface for dub doesn't make the difference between build type
//and config, instead each possible combination type + build is generated.
if fJSON.Find('configurations') <> nil then
begin
arr := fJSON.Arrays['configurations'];
for i:= 0 to arr.Count-1 do
begin
item := TJSONObject(arr.Items[i]);
if item.Find('name') = nil then
continue;
fConfigs.Add(item.Strings['name']);
end;
end else
begin
fConfigs.Add(DubDefaultConfigName);
// default = what dub set as 'application' or 'library'
// in this case Coedit will pass only the type to DUB: 'DUB --build=release'
end;
fBuildTypes.AddStrings(DubBuiltTypeName);
dat := fJSON.Find('buildTypes');
if assigned(dat) and (dat.JSONType = jtObject) then
begin
obj := fJSON.Objects['buildTypes'];
for i := 0 to obj.Count-1 do
begin
itemname := obj.Names[i];
// defaults build types can be overridden
if fBuildTypes.IndexOf(itemname) <> -1 then
continue;
fBuildTypes.Add(itemname);
end;
end;
fConfigsCount := fConfigs.Count * fBuildTypes.Count;
end;
procedure TCEDubProject.updateSourcesFromJson;
var
lst: TStringList;
item: TJSONData;
conf: TJSONObject;
arr: TJSONArray;
i, j: integer;
procedure getExclusion(from: TJSONObject);
var
i: integer;
begin
item := from.Find('excludedSourceFiles');
if assigned(item) and (item.JSONType = jtArray) then
begin
arr := TJSONArray(item);
for i := 0 to arr.Count-1 do
lst.Add(patchPlateformPath(arr.Strings[i]));
end;
end;
procedure tryAddFromFolder(const pth: string);
var
abs: string;
begin
if DirectoryExists(pth) then
begin
lst.Clear;
listFiles(lst, pth, true);
for abs in lst do
if isDlangCompilable(extractFileExt(abs)) then
fSrcs.Add(ExtractRelativepath(fBasePath, abs));
end;
end;
var
pth: string;
begin
fSrcs.Clear;
if not assigned(fJSON) then
exit;
lst := TStringList.Create;
try
// auto folders & files
item := fJSON.Find('mainSourceFile');
if assigned(item) then
fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, item.AsString)));
tryAddFromFolder(fBasePath + 'src');
tryAddFromFolder(fBasePath + 'source');
// custom folders
item := fJSON.Find('sourcePaths');
if assigned(item) then
begin
arr := TJSONArray(item);
for i := 0 to arr.Count-1 do
begin
pth := TrimRightSet(arr.Strings[i], ['/','\']);
if DirectoryExists(pth) then
tryAddFromFolder(pth)
else
tryAddFromFolder(fBasePath + pth);
end;
end;
// custom files
item := fJSON.Find('sourceFiles');
if assigned(item) then
begin
arr := TJSONArray(item);
for i := 0 to arr.Count-1 do
fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, arr.Strings[i])));
end;
conf := getCurrentCustomConfig;
if assigned(conf) then
begin
item := conf.Find('mainSourceFile');
if assigned(item) then
fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, item.AsString)));
// custom folders in current config
item := conf.Find('sourcePaths');
if assigned(item) then
begin
arr := TJSONArray(item);
for i := 0 to arr.Count-1 do
begin
pth := TrimRightSet(arr.Strings[i], ['/','\']);
if DirectoryExists(pth) then
tryAddFromFolder(pth)
else
tryAddFromFolder(fBasePath + pth);
end;
end;
// custom files in current config
item := conf.Find('sourceFiles');
if assigned(item) then
begin
arr := TJSONArray(item);
for i := 0 to arr.Count-1 do
fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, arr.Strings[i])));
end;
end;
deleteDups(fSrcs);
// exclusions
lst.Clear;
getExclusion(fJSON);
conf := getCurrentCustomConfig;
if assigned(conf) then
getExclusion(conf);
for i := fSrcs.Count-1 downto 0 do
for j := 0 to lst.Count-1 do
if SameFileName(fSrcs[i], lst[j]) then
fSrcs.Delete(i);
// TODO-cDUB: manage exclusions with http://dlang.org/phobos/std_path.html#.globMatch
finally
lst.Free;
end;
end;
function TCEDubProject.findTargetKindInd(value: TJSONObject): boolean;
var
tt: TJSONData;
begin
result := true;
if value.Find('mainSourceFile') <> nil then
begin
fBinKind := executable;
exit;
end;
tt := value.Find('targetType');
if tt <> nil then
begin
case tt.AsString of
'executable': fBinKind := executable;
'staticLibrary', 'library' : fBinKind := staticlib;
'dynamicLibrary' : fBinKind := sharedlib;
'autodetect': result := false;
else fBinKind := executable;
end;
end else result := false;
end;
procedure TCEDubProject.updateTargetKindFromJson;
var
found: boolean = false;
conf: TJSONObject;
src: string;
begin
fBinKind := executable;
if not assigned(fJSON) then exit;
// note: in Coedit this is only used to known if output can be launched
found := findTargetKindInd(fJSON);
conf := getCurrentCustomConfig;
if assigned(conf) then
found := found or findTargetKindInd(conf);
if not found then
begin
for src in fSrcs do
begin
if (src = 'source' + DirectorySeparator + 'app.d')
or (src = 'src' + DirectorySeparator + 'app.d')
or (src = 'source' + DirectorySeparator + 'main.d')
or (src = 'src' + DirectorySeparator + 'main.d')
or (src = 'source' + DirectorySeparator + fPackageName + DirectorySeparator + 'app.d')
or (src = 'src' + DirectorySeparator + fPackageName + DirectorySeparator + 'app.d')
or (src = 'source' + DirectorySeparator + fPackageName + DirectorySeparator + 'main.d')
or (src = 'src' + DirectorySeparator + fPackageName + DirectorySeparator + 'main.d')
then fBinKind:= executable
else fBinKind:= staticlib;
end;
end;
end;
procedure TCEDubProject.updateImportPathsFromJson;
procedure addFrom(obj: TJSONObject);
var
arr: TJSONArray;
item: TJSONData;
i: integer;
begin
item := obj.Find('importPaths');
if assigned(item) then
begin
arr := TJSONArray(item);
for i:= 0 to arr.Count-1 do
fImportPaths.Add(arr.Strings[i]);
end;
end;
var
conf: TJSONObject;
begin
if not assigned(fJSON) then exit;
//
addFrom(fJSON);
conf := getCurrentCustomConfig;
if assigned(conf) then addFrom(conf);
end;
procedure TCEDubProject.updateOutputNameFromJson;
var
conf: TJSONObject;
item: TJSONData;
namePart, pathPart: string;
procedure setFrom(obj: TJSONObject);
var
n,p: TJSONData;
begin
p := obj.Find('targetPath');
n := obj.Find('targetName');
if assigned(p) then pathPart := p.AsString;
if assigned(n) then namePart := n.AsString;
end;
begin
fOutputFileName := '';
if not assigned(fJSON) then exit;
item := fJSON.Find('name');
if not assigned(item) then
exit;
namePart := item.AsString;
pathPart := fBasePath;
setFrom(fJSON);
conf := getCurrentCustomConfig;
if assigned(conf) then
setFrom(conf);
pathPart := TrimRightSet(pathPart, ['/','\']);
{$IFDEF WINDOWS}
{$ELSE}
if fBinKind in [staticlib, sharedlib] then
namePart := 'lib' + namePart;
{$ENDIF}
fOutputFileName:= pathPart + DirectorySeparator + namePart;
patchPlateformPath(fOutputFileName);
fOutputFileName := expandFilenameEx(fBasePath, fOutputFileName);
case fBinKind of
executable: fOutputFileName += exeExt;
staticlib: fOutputFileName += libExt;
obj: fOutputFileName += objExt;
sharedlib: fOutputFileName += dynExt;
end;
end;
procedure TCEDubProject.updateFields;
begin
updatePackageNameFromJson;
udpateConfigsFromJson;
updateSourcesFromJson;
updateTargetKindFromJson;
updateImportPathsFromJson;
updateOutputNameFromJson;
end;
procedure TCEDubProject.beginModification;
begin
fModificationCount += 1;
end;
procedure TCEDubProject.endModification;
begin
fModificationCount -=1;
if fModificationCount <= 0 then
doModified;
end;
procedure TCEDubProject.doModified;
begin
fModificationCount := 0;
fModified:=true;
updateFields;
subjProjChanged(fProjectSubject, self as ICECommonProject);
end;
{$ENDREGION}
function isValidDubProject(const filename: string): boolean;
var
maybe: TCEDubProject;
begin
result := true;
// avoid the project to notify the observers, current project is not replaced
EntitiesConnector.beginUpdate;
maybe := TCEDubProject.create(nil);
try
try
maybe.loadFromFile(filename);
if maybe.json = nil then
result := false
else if maybe.json.Find('name') = nil then
result := false;
except
result := false;
end;
finally
maybe.Free;
EntitiesConnector.endUpdate;
end;
end;
{$ENDREGION --------------------------------------------------------------------}
end.