unit ce_dubproject;

{$I ce_defines.inc}

interface

uses
  Classes, SysUtils, fpjson, jsonparser, jsonscanner, process, strutils,
  LazFileUtils, RegExpr, fgl,
  ce_common, ce_interfaces, ce_observer, ce_dialogs, ce_processes,
  ce_writableComponent, ce_compilers, ce_semver, ce_stringrange;

type

  TDubLinkMode = (dlmSeparate, dlmAllAtOnce, dlmSingleFile);

  TDubDependencyCheck = (dcStandard, dcOffline, dcNo);

  TDubVerbosity = (default, quiet, verbose, veryVerbose, onlyWarnAndError, onlyError);

  TDubArchOverride = (auto, x86, x86_64);

  PDubLocalPackage = ^TDubLocalPackage;

  TSemVerList = specialize TFPGList<PSemVer>;

  TDubLocalPackage = class
  strict private
    fName : string;
    fVersions: TSemVerList;
  public
    constructor create;
    destructor destroy; override;
    procedure addVersion(const value: string);
    function findVersion(constref value: TSemVer): PSemVer;
    function highestInInterval(constref lo, hi: TSemVer): PSemVer;
    function highest: PSemVer;
    property name: string read fName write fName;
  end;

  TDubLocalPackages = class
  strict private
    fRoot: string;
    fLocalPackages: array of TDubLocalPackage;
  public
    destructor destroy; override;
    procedure update;
    function find(const name: string; out package: PDubLocalPackage): boolean; overload;
    function find(const name, op: string; constref opVer: TSemVer;
      out package: PDubLocalPackage): PSemver; overload;
    function fetch(constref version: TSemVer): PDubLocalPackage;
    function getPackageePath(package: PDubLocalPackage): string;
  end;

  (**
   * Stores the build options, always applied when a project is build
   *)
  TCEDubBuildOptionsBase = class(TWritableLfmTextComponent)
  strict private
    fParallel: boolean;
    fForceRebuild: boolean;
    fLinkMode: TDubLinkMode;
    fCombined: boolean;
    fDepCheck: TDubDependencyCheck;
    fVerbosity: TDubVerbosity;
    fArchOverride: TDubArchOverride;
    fOther: string;
    fCompiler: DCompiler;
    fShowConsole: boolean;
    fAutoFetch: boolean;
    procedure setLinkMode(value: TDubLinkMode);
    procedure setCompiler(value: DCompiler);
    function getCompiler: DCompiler;
  published
    property showConsole: boolean read fShowConsole write fShowConsole default false;
    property compiler: DCompiler read getCompiler write setCompiler;
    property parallel: boolean read fParallel write fParallel;
    property forceRebuild: boolean read fForceRebuild write fForceRebuild;
    property linkMode: TDubLinkMode read fLinkMode write setLinkMode;
    property combined: boolean read fCombined write fCombined;
    property other: string read fOther write fOther;
    property dependenciesCheck: TDubDependencyCheck read fDepCheck write fDepCheck;
    property verbosity: TDubVerbosity read fVerbosity write fVerbosity default default;
    property archOverride: TDubArchOverride read fArchOverride write fArchOverride default auto;
    property autoFetch: boolean read fAutoFetch write fAutoFetch default false;
  public
    procedure assign(source: TPersistent); override;
    procedure getOpts(options: TStrings);
  end;

  (**
   * Make the build options editable
   *)
  TCEDubBuildOptions = class(TCEDubBuildOptionsBase, ICEEditableOptions)
  strict private
    fBackup: TCEDubBuildOptionsBase;
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
  end;

  TCEDubProject = class(TComponent, ICECommonProject)
  private
    fIsSdl: boolean;
    fInGroup: boolean;
    fDubProc: TCEProcess;
    fPreCompilePath: string;
    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;
    fSaveAsUtf8: boolean;
    fCompiled: boolean;
    fMsgs: ICEMessagesDisplay;
    fLocalPackages: TDubLocalPackages;
    procedure doModified;
    procedure updateFields;
    procedure updatePackageNameFromJson;
    procedure udpateConfigsFromJson;
    procedure updateSourcesFromJson;
    procedure updateTargetKindFromJson;
    procedure updateImportPathsFromJson;
    procedure updateOutputNameFromJson;
    function findTargetKindInd(value: TJSONObject): boolean;
    procedure dubProcOutput(proc: TObject);
    procedure dubProcTerminated(proc: TObject);
    function getCurrentCustomConfig: TJSONObject;
    procedure compileOrRun(run: boolean; const runArgs: string = '');
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    //
    procedure beginModification;
    procedure endModification;
    //
    function filename: string;
    function basePath: string;
    procedure loadFromFile(const fname: string);
    procedure saveToFile(const fname: string);
    //
    procedure updateSourcesList;
    procedure activate;
    function inGroup: boolean;
    procedure inGroup(value: boolean);
    function getFormat: TCEProjectFormat;
    function getProject: TObject;
    function modified: boolean;
    function binaryKind: TProjectBinaryKind;
    function getCommandLine: string;
    function outputFilename: string;
    procedure reload;
    //
    function isSource(const fname: 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;
    //
    procedure compile;
    function compiled: boolean;
    procedure run(const runArgs: string = '');
    function targetUpToDate: boolean;
    //
    property json: TJSONObject read fJSON;
    property packageName: string read fPackageName;
    property isSDL: boolean read fIsSdl;
  end;

  // these 9 built types always exist
  TDubBuildType = (plain, debug, release, releaseDebug, releaseNoBounds, 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;

  // converts a sdl description to json, returns the json
  function sdl2json(const filename: string): TJSONObject;

  function getDubCompiler: DCompiler;
  procedure setDubCompiler(value: DCompiler);

var
  DubCompiler: DCompiler = dmd;
  DubCompilerFilename: string = 'dmd';

const
  DubSdlWarning = 'this feature is deactivated in DUB projects with the SDL format';

implementation

var
  dubBuildOptions: TCEDubBuildOptions;

const

  optFname = 'dubbuild.txt';

  DubBuiltTypeName: array[TDubBuildType] of string = ('plain', 'debug', 'release',
    'release-debug', 'release-nobounds', 'unittest', 'docs', 'ddox', 'profile',
    'cov', 'unittest-cov'
  );

  DubDefaultConfigName = '(default config)';

{$REGION TDubLocalPackages -----------------------------------------------------}
constructor TDubLocalPackage.create;
begin
  fVersions := TSemVerList.create;
end;

destructor TDubLocalPackage.destroy;
var
  i: integer;
begin
  for i := 0 to fVersions.Count-1 do
    dispose(fVersions.Items[i]);
  fVersions.Free;
  inherited;
end;

procedure TDubLocalPackage.addVersion(const value: string);
var
  v: PSemVer;
  i: integer;
begin
  v := new(PSemVer);
  if value = 'vmaster' then
    v^.init('v0.0.0-master')
  else try
    v^.init(value);
  except
    dispose(v);
    exit;
  end;
  for i := 0 to fVersions.Count-1 do
  begin
    if fVersions[i]^ = v^ then
      exit;
    if (i < fVersions.Count-1) and (fVersions[i+1]^ > v^) and (fVersions[i]^ < v^ ) then
    begin
      fVersions.Insert(i, v);
      exit;
    end;
  end;
  fVersions.Add(v);
end;

function TDubLocalPackage.highest: PSemVer;
begin
  result := fVersions.Last;
end;

function TDubLocalPackage.highestInInterval(constref lo, hi: TSemVer): PSemVer;
var
  i: integer;
begin
  result := nil;
  for i := 0 to fVersions.Count-1 do
  begin
    if fVersions[i]^ < lo then
      continue;
    if fVersions[i]^ < hi then
      result := fVersions[i];
    if (fVersions[i]^ > hi) then
      break;
  end;
end;

function TDubLocalPackage.findVersion(constref value: TSemVer): PSemVer;
var
  i: integer;
begin
  result := nil;
  for i:= 0 to fVersions.Count-1 do
    if fVersions.Items[i]^ = value then
      exit(fVersions.Items[i]);
end;

destructor TDubLocalPackages.destroy;
var
  i: integer;
begin
  for i:= 0 to high(fLocalPackages) do
    fLocalPackages[i].Free;
  inherited;
end;

procedure TDubLocalPackages.update;
var
  p: TStringList;
  s: string;
  n: string;
  v: string = '';
  i: integer;
  j: integer = 0;
  d: PDubLocalPackage = nil;
  h: TStringRange = (ptr: nil; pos: 0; len: 0);
begin
  setLength(fLocalPackages, 0);
  {$IFDEF WINDOWS}
  fRoot := GetEnvironmentVariable('APPDATA') + '\dub\packages\';
  {$ELSE}
  fRoot := GetEnvironmentVariable('HOME') + '/.dub/packages/';
  {$ENDIF}
  if not fRoot.dirExists then
    exit;

  p := TStringList.Create;
  try
    listFolders(p, fRoot);
    for i := 0 to p.Count-1 do
    begin
      j := 0;
      s := p[i];
      h.init(s);
      while true do
      begin
        h.popUntil('-');
        if h.empty then
          break;
        if (h.popFront^.front in ['0'..'9']) or
            h.endsWith('master') then
        begin
          j := h.position;
          break;
        end;
      end;
      if (j = 0) then
        continue;

      n := s[1..j-1];
      n := n.extractFileName;
      if not find(n, d) then
      begin
        setLength(fLocalPackages, length(fLocalPackages) + 1);
        fLocalPackages[high(fLocalPackages)] := TDubLocalPackage.create;
        d := @fLocalPackages[high(fLocalPackages)];
        d^.name := n;
      end;
      v := 'v' + s[j+1 .. length(s)];
      d^.addVersion(v);

    end;
  finally
    p.Free;
  end;
end;

function TDubLocalPackages.find(const name: string; out package: PDubLocalPackage): boolean;
var
  i: integer;
begin
  result := false;
  package:= nil;
  for i := 0 to high(fLocalPackages) do
    if fLocalPackages[i].name = name then
  begin
    result := true;
    package := @fLocalPackages[i];
    break;
  end;
end;

function TDubLocalPackages.find(const name, op: string; constref opVer: TSemVer;
  out package: PDubLocalPackage): PSemVer;
var
  hi: TSemVer;
begin
  result := nil;
  if op = '=' then
  begin
    if find(name, package) then
      result := package^.findVersion(opVer);
  end
  else if op = '>=' then
  begin
    if find(name, package) then
    begin
      result := package^.highest;
      if result^ < opVer then
        result := nil;
    end;
  end
  else if op = '>' then
  begin
    if find(name, package) then
    begin
      result := package^.highest;
      if (result^ < opVer) or (result^ = opVer) then
        result := nil;
    end;
  end
  else if op = '~>' then
  begin
    if find(name, package) then
    begin
      hi := opVer;
      hi.minor := hi.minor + 1;
      hi.patch := 0;
      hi.additional :='';
      result := package^.highestInInterval(opVer, hi);
      result := result;
    end;
  end
  else
  begin
    if find(name, package) then
      result := package^.highest;
  end;
end;

function TDubLocalPackages.fetch(constref version: TSemVer): PDubLocalPackage;
begin
  result := nil;
end;

function TDubLocalPackages.getPackageePath(package: PDubLocalPackage): string;
begin
  result := fRoot + package^.Name;
end;
{$ENDREGION}

{$REGION Options ---------------------------------------------------------------}
procedure TCEDubBuildOptionsBase.setLinkMode(value: TDubLinkMode);
begin
  if fLinkMode = value then
    exit;
  if not (value in [low(TDubLinkMode)..high(TDubLinkMode)]) then
    value := low(TDubLinkMode);
  fLinkMode:=value;
end;

procedure TCEDubBuildOptionsBase.setCompiler(value: DCompiler);
begin
  fCompiler := value;
  setDubCompiler(fCompiler);
end;

function TCEDubBuildOptionsBase.getCompiler: DCompiler;
begin
  result := fCompiler;
end;

procedure TCEDubBuildOptionsBase.assign(source: TPersistent);
var
  opts: TCEDubBuildOptionsBase;
begin
  if source is TCEDubBuildOptionsBase then
  begin
    opts := TCEDubBuildOptionsBase(source);
    parallel:=opts.parallel;
    forceRebuild:=opts.forceRebuild;
    combined:=opts.combined;
    linkMode:=opts.linkMode;
    other:=opts.other;
    dependenciesCheck:=opts.dependenciesCheck;
    compiler:=opts.compiler;
    verbosity:=opts.verbosity;
    archOverride:=opts.archOverride;
    autoFetch:=opts.autoFetch;
  end
  else inherited;
end;

procedure TCEDubBuildOptionsBase.getOpts(options: TStrings);
const
  vb: array[TDubVerbosity] of string = (
  '',           //auto,
  '--vquiet',   //quiet,
  '-v',         //verbose,
  '--vverbose', //veryVerbose,
  '-q',         //onlyWarnAndError,
  '--verror');  //vError
  ao: array [TDubArchOverride] of string = (
  '',
  '--arch=x86',
  '--arch=x86_64'
  );
begin
  if parallel then
    options.Add('--parallel');
  if forceRebuild then
    options.Add('--force');
  if combined then
    options.Add('--combined');
  case linkMode of
    dlmAllAtOnce: options.Add('--build-mode=allAtOnce');
    dlmSingleFile: options.Add('--build-mode=singleFile');
  end;
  case dependenciesCheck of
    dcNo: options.Add('--skip-registry=all');
    dcOffline: options.Add('--skip-registry=standard');
  end;
  if fVerbosity <> TDubVerbosity.default then
    options.Add(vb[fVerbosity]);
  if fArchOverride <> TDubArchOverride.auto then
    options.Add(ao[fArchOverride]);
  if other.isNotEmpty then
    CommandToList(other, options);
end;

constructor TCEDubBuildOptions.create(aOwner: TComponent);
var
  fname: string;
begin
  inherited;
  fBackup := TCEDubBuildOptionsBase.Create(nil);
  EntitiesConnector.addObserver(self);
  fname := getCoeditDocPath + optFname;
  if fname.fileExists then
    loadFromFile(fname);
end;

destructor TCEDubBuildOptions.destroy;
begin
  saveToFile(getCoeditDocPath + optFname);
  EntitiesConnector.removeObserver(self);
  fBackup.free;
  inherited;
end;

function TCEDubBuildOptions.optionedWantCategory(): string;
begin
  exit('DUB build');
end;

function TCEDubBuildOptions.optionedWantEditorKind: TOptionEditorKind;
begin
  exit(oekGeneric);
end;

function TCEDubBuildOptions.optionedWantContainer: TPersistent;
begin
  exit(self);
  fBackup.assign(self);
end;

procedure TCEDubBuildOptions.optionedEvent(event: TOptionEditorEvent);
begin
  case event of
    oeeAccept: fBackup.assign(self);
    oeeCancel: self.assign(fBackup);
    oeeSelectCat:fBackup.assign(self);
  end;
end;

function TCEDubBuildOptions.optionedOptionsModified: boolean;
begin
  exit(false);
end;
{$ENDREGION}

{$REGION Standard Comp/Obj -----------------------------------------------------}
constructor TCEDubProject.create(aOwner: TComponent);
begin
  inherited;
  fSaveAsUtf8 := true;
  fJSON := TJSONObject.Create();
  fProjectSubject := TCEProjectSubject.Create;
  fMsgs:= getMessageDisplay;
  fBuildTypes := TStringList.Create;
  fConfigs := TStringList.Create;
  fSrcs := TStringList.Create;
  fSrcs.Sorted:=true;
  fSrcs.Duplicates:=dupIgnore;
  fImportPaths := TStringList.Create;
  fImportPaths.Sorted:=true;
  fImportPaths.Duplicates:=dupIgnore;

  json.Add('name', '');
  endModification;
  subjProjNew(fProjectSubject, self);
  doModified;
  fModified:=false;

  fLocalPackages := TDubLocalPackages.Create;
  fLocalPackages.update;
end;

destructor TCEDubProject.destroy;
begin
  killProcess(fDubProc);
  subjProjClosing(fProjectSubject, self);
  fProjectSubject.free;

  fJSON.Free;
  fBuildTypes.Free;
  fConfigs.Free;
  fSrcs.Free;
  fImportPaths.Free;
  fLocalPackages.Free;
  inherited;
end;
{$ENDREGION --------------------------------------------------------------------}

{$REGION ICECommonProject: project props ---------------------------------------}
procedure TCEDubProject.activate;
begin
  subjProjFocused(fProjectSubject, self as ICECommonProject);
end;

function TCEDubProject.inGroup: boolean;
begin
  exit(fInGroup);
end;

procedure TCEDubProject.inGroup(value: boolean);
begin
  fInGroup:=value;
end;


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.reload;
begin
  if fFilename.fileExists then
    loadFromFile(fFilename);
end;

procedure TCEDubProject.loadFromFile(const fname: string);
var
  loader: TMemoryStream;
  parser : TJSONParser;
  ext: string;
  bom: dword = 0;
begin
  ext := fname.extractFileExt.upperCase;
  fBasePath := fname.extractFilePath;
  fFilename := fname;
  fSaveAsUtf8 := false;
  fIsSdl := false;
  if ext = '.JSON' then
  begin
    loader := TMemoryStream.Create;
    try
      loader.LoadFromFile(fFilename);
      // skip BOMs, they crash the parser
      loader.Read(bom, 4);
      if (bom and $BFBBEF) = $BFBBEF then
      begin
        loader.Position:= 3;
        fSaveAsUtf8 := true;
      end
      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;
        fFilename := '';
      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;
        fFilename := '';
      end
      else
        loader.Position:= 0;
      //
      FreeAndNil(fJSON);
      parser := TJSONParser.Create(loader, [joIgnoreTrailingComma, joUTF8]);
      try
        try
          fJSON := parser.Parse as TJSONObject;
        except
          if assigned(fJSON) then
            FreeAndNil(fJSON);
          fFilename := '';
        end;
      finally
        parser.Free;
      end;
    finally
      loader.Free;
    end;
  end
  else if ext = '.SDL' then
  begin
    FreeAndNil(fJSON);
    fJSON := sdl2json(fFilename);
    if fJSON.isNil then
      fFilename := ''
    else
      fIsSdl := true;
  end;

  if not assigned(fJSON) then
    fJson := TJSONObject.Create(['name','invalid json']);

  updateFields;
  subjProjChanged(fProjectSubject, self);
  fModified := false;
end;

procedure TCEDubProject.saveToFile(const fname: string);
var
  saver: TMemoryStream;
  str: string;
begin
  if fname <> fFilename then
    inGroup(false);
  saver := TMemoryStream.Create;
  try
    fFilename := fname;
    str := fJSON.FormatJSON;
    if fSaveAsUtf8 then
    begin
      saver.WriteDWord($00BFBBEF);
      saver.Position:=saver.Position-1;
    end;
    saver.Write(str[1], str.length);
    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[fBuiltTypeIx]);
    if (fConfigs.Count <> 1) and (fConfigs[0] <> DubDefaultConfigName) then
      str.Add('--config=' + fConfigs[fConfigIx]);
    str.Add('--compiler=' + DubCompilerFilename);
    dubBuildOptions.getOpts(str);
    result := str.Text;
  finally
    str.Free;
  end;
end;

function TCEDubProject.outputFilename: string;
begin
  exit(fOutputFileName);
end;
{$ENDREGION --------------------------------------------------------------------}

{$REGION ICECommonProject: sources ---------------------------------------------}
function TCEDubProject.isSource(const fname: string): boolean;
var
  str: string;
begin
  str := fname;
  if str.fileExists then
    str := ExtractRelativepath(fBasePath, str);
  result := fSrcs.IndexOf(str) <> -1;
end;

function TCEDubProject.sourcesCount: integer;
begin
  exit(fSrcs.Count);
end;

function TCEDubProject.sourceRelative(index: integer): string;
begin
  exit(fSrcs[index]);
end;

function TCEDubProject.sourceAbsolute(index: integer): string;
var
  fname: string;
begin
  fname := fSrcs[index];
  if fname.fileExists 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[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[index div fConfigs.Count] + ' - ' +
    fConfigs[index mod fConfigs.Count];
end;
{$ENDREGION --------------------------------------------------------------------}

{$REGION ICECommonProject: actions ---------------------------------------------}
procedure TCEDubProject.dubProcOutput(proc: TObject);
var
  lst: TStringList;
  str: string;
begin
  lst := TStringList.Create;
  try
    fDubProc.getFullLines(lst);
    for str in lst do
      fMsgs.message(str, self as ICECommonProject, amcProj, amkAuto);
  finally
    lst.Free;
  end;
end;

procedure TCEDubProject.dubProcTerminated(proc: TObject);
var
  prjname: string;
begin
  dubProcOutput(proc);
  prjname := shortenPath(filename);
  fCompiled := fDubProc.ExitStatus = 0;
  if fCompiled then
    fMsgs.message(prjname + ' has been successfully compiled',
      self as ICECommonProject, amcProj, amkInf)
  else
  begin
    fMsgs.message(prjname + ' has not been compiled',
      self as ICECommonProject, amcProj, amkWarn);
    fMsgs.message(format('error: DUB has returned the status %s',
      [prettyReturnStatus(fDubProc)]), self as ICECommonProject, amcProj, amkErr);
  end;
  subjProjCompiled(fProjectSubject, self as ICECommonProject, fCompiled);
  SetCurrentDirUTF8(fPreCompilePath);
end;

procedure TCEDubProject.compileOrRun(run: boolean; const runArgs: string = '');
var
  olddir: string;
  prjname: string;
  rargs: TStringList;
begin
  if fDubProc.isNotNil and fDubProc.Active then
  begin
    fMsgs.message('the project is already being compiled',
      self as ICECommonProject, amcProj, amkWarn);
    exit;
  end;
  killProcess(fDubProc);
  fCompiled := false;
  if not fFilename.fileExists then
  begin
    dlgOkInfo('The DUB project must be saved before being compiled or run !');
    exit;
  end;
  fMsgs.clearByData(Self as ICECommonProject);
  prjname := shortenPath(fFilename);
  fDubProc:= TCEProcess.Create(nil);
  olddir  := GetCurrentDir;
  try
    if not run then
    begin
      subjProjCompiling(fProjectSubject, self as ICECommonProject);
      fMsgs.message('compiling ' + prjname, self as ICECommonProject, amcProj, amkInf);
      if modified then saveToFile(fFilename);
    end;
    chDir(fFilename.extractFilePath);
    fDubProc.Executable := 'dub' + exeExt;
    if not dubBuildOptions.showConsole then
    begin
      fDubProc.Options := fDubProc.Options + [poStderrToOutPut, poUsePipes];
      fDubProc.OnReadData:= @dubProcOutput;
      fDubProc.ShowWindow := swoHIDE;
    end
    else
    begin
      fDubProc.Options := fDubProc.Options + [poWaitOnExit, poNewConsole];
    end;
    fDubProc.CurrentDirectory := fFilename.extractFilePath;
    fDubProc.XTermProgram:=consoleProgram;
    if not run then
    begin
      fDubProc.Parameters.Add('build');
      fDubProc.OnTerminate:= @dubProcTerminated;
    end
    else
    begin
      fDubProc.Parameters.Add('run');
      fDubProc.OnTerminate:= @dubProcOutput;
    end;
    fDubProc.Parameters.Add('--build=' + fBuildTypes[fBuiltTypeIx]);
    if (fConfigs.Count <> 1) and (fConfigs[0] <> DubDefaultConfigName) then
      fDubProc.Parameters.Add('--config=' + fConfigs[fConfigIx]);
    fDubProc.Parameters.Add('--compiler=' + DubCompilerFilename);
    dubBuildOptions.getOpts(fDubProc.Parameters);
    if run and runArgs.isNotEmpty then
    begin
      fDubProc.Parameters.Add('--');
      rargs := TStringList.Create;
      try
        CommandToList(runArgs, rargs);
        fDubProc.Parameters.AddStrings(rargs);
      finally
        rargs.Free;
      end;
    end;
    fDubProc.Execute;
  finally
    SetCurrentDirUTF8(olddir);
  end;
end;

procedure TCEDubProject.compile;
begin
  fPreCompilePath := GetCurrentDirUTF8;
  compileOrRun(false);
end;

function TCEDubProject.compiled: boolean;
begin
  exit(fCompiled);
end;

procedure TCEDubProject.run(const runArgs: string = '');
begin
  compileOrRun(true, runArgs);
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
  confs: TJSONArray;
begin
  result := nil;
  if fJSON.findArray('configurations', confs) and (fConfigIx < confs.Count) then
    result := confs.Objects[fConfigIx];
end;

procedure TCEDubProject.updatePackageNameFromJson;
var
  value: TJSONData;
begin
  if fJSON.isNil then
    exit;
  if not fJSON.findAny('name', 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 fJSON.isNil 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.findArray('configurations', arr) and (arr.Count > 0) then
  begin
    for i:= 0 to arr.Count-1 do
    begin
      item := TJSONObject(arr.Items[i]);
      if item.findAny('name', dat) then
        fConfigs.Add(dat.AsString);
    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);
  if fJSON.findObject('buildTypes', obj) then 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;

  deleteDups(fConfigs);
  deleteDups(fBuildTypes);
  fConfigsCount := fConfigs.Count * fBuildTypes.Count;
end;

procedure TCEDubProject.updateSourcesList;
begin
  updateSourcesFromJson;
end;

procedure TCEDubProject.updateSourcesFromJson;
var
  lst: TStringList;
  item: TJSONData;
  conf: TJSONObject;
  arr: TJSONArray;
  i{, j}: integer;
procedure getExclusion(from: TJSONObject);
var
  i: integer;
begin
  if from.findArray('excludedSourceFiles', arr) then
    for i := 0 to arr.Count-1 do
      lst.Add(patchPlateformPath(arr.Strings[i]));
end;
procedure tryAddRelOrAbsFile(const fname: string);
begin
  if not isDlangCompilable(fname.extractFileExt) then
    exit;
  if fname.fileExists and FilenameIsAbsolute(fname) then
  begin
    fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, fname)))
  end
  else if patchPlateformPath(expandFilenameEx(fBasePath, fname)).fileExists then
    fSrcs.Add(patchPlateformPath(fname));
end;
procedure tryAddFromFolder(const pth: string);
var
  abs: string;
begin
  if pth.dirExists then
  begin
    lst.Clear;
    listFiles(lst, pth, true);
    for abs in lst do
      if isDlangCompilable(abs.extractFileExt) then
        fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, abs)));
  end;
end;
var
  pth: string;
  //glb: TRegExpr;
begin
  fSrcs.Clear;
  if not assigned(fJSON) then
    exit;
  lst := TStringList.Create;
  try
    // auto folders & files
    if fJSON.findAny('mainSourceFile', item) then
    begin
      pth := item.AsString;
      if pth.fileExists then
        fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, pth)))
      else if expandFilenameEx(fBasePath, pth).fileExists then
        fSrcs.Add(patchPlateformPath(pth));
    end;
    tryAddFromFolder(fBasePath + 'src');
    tryAddFromFolder(fBasePath + 'source');
    // custom folders
    if fJSON.findArray('sourcePaths', arr) then for i := 0 to arr.Count-1 do
    begin
      pth := TrimRightSet(arr.Strings[i], ['/','\']);
      if pth.dirExists and FilenameIsAbsolute(pth) then
        tryAddFromFolder(pth)
      else
        tryAddFromFolder(expandFilenameEx(fBasePath, pth));
    end;
    // custom files
    if fJSON.findArray('sourceFiles', arr) then for i := 0 to arr.Count-1 do
      tryAddRelOrAbsFile(arr.Strings[i]);
    conf := getCurrentCustomConfig;
    if conf.isNotNil then
    begin
      if conf.findAny('mainSourceFile', item) then
      begin
        pth := item.AsString;
        if pth.fileExists then
          fSrcs.Add(patchPlateformPath(ExtractRelativepath(fBasePath, pth)))
        else if expandFilenameEx(fBasePath, pth).fileExists then
          fSrcs.Add(patchPlateformPath(pth));
      end;
      // custom folders in current config
      if conf.findArray('sourcePaths', arr) then for i := 0 to arr.Count-1 do
      begin
        pth := TrimRightSet(arr.Strings[i], ['/','\']);
        if pth.dirExists and FilenameIsAbsolute(pth) then
          tryAddFromFolder(pth)
        else
          tryAddFromFolder(expandFilenameEx(fBasePath, pth));
      end;
      // custom files in current config
      if conf.findArray('sourceFiles', arr) then for i := 0 to arr.Count-1 do
        tryAddRelOrAbsFile(arr.Strings[i]);
    end;
    // exclusions : not managed anymore because of other IDE features that rely
    // on the full list (scan TODOs, <CPFS>, search in project, etc)
    {lst.Clear;
    getExclusion(fJSON);
      conf := getCurrentCustomConfig;
    if conf.isNotNil then
      getExclusion(conf);
    if lst.Count > 0 then
    begin
      glb := TRegExpr.Create;
      try
        for j := 0 to lst.Count-1 do
        begin
          try
            glb.Expression := globToReg(lst[j]);
            glb.Compile;
            for i := fSrcs.Count-1 downto 0 do
              if glb.Exec(fSrcs[i]) then
                fSrcs.Delete(i);
          except
            continue;
          end;
        end;
      finally
        glb.Free;
      end;
    end;}
  finally
    lst.Free;
  end;
  deleteDups(fSrcs);
end;

function TCEDubProject.findTargetKindInd(value: TJSONObject): boolean;
var
  tt: TJSONData;
begin
  result := true;
  if value.Find('mainSourceFile').isNotNil then
    fBinKind := executable
  else if value.findAny('targetType', tt) 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 fJSON.isNil then exit;
  // note: in Coedit this is only used to known if output can be launched
  found := findTargetKindInd(fJSON);
  conf := getCurrentCustomConfig;
  if conf.isNotNil 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
      begin
        fBinKind:= executable;
        break;
      end
      else
        fBinKind:= staticlib;
    end;
  end;
end;

procedure TCEDubProject.updateImportPathsFromJson;

  procedure addFrom(obj: TJSONObject);
  var
    arr: TJSONArray;
    pth: string;
    i: integer;
  begin
    if obj.findArray('importPaths', arr) then for i := 0 to arr.Count-1 do
    begin
      pth := TrimRightSet(arr.Strings[i], ['/','\']);
      if pth.dirExists and FilenameIsAbsolute(pth) then
        fImportPaths.Add(pth)
      else
        fImportPaths.Add(expandFilenameEx(fBasePath, pth));
    end;
  end;

  // note: dependencies are added as import to allow DCD completion
  // see TCEDcdWrapper.projChanged()
  procedure addDepsFrom(obj: TJSONObject; const suffix: string = '');
  var
    deps: TJSONObject;
    pck: PDubLocalPackage;
    j: TJSONData;
    p: string;
    s: string;
    v: string;
    n: string;
    o: string;
    z: string;
    r: TStringRange = (ptr: nil; pos: 0; len: 0);
    q: TSemVer;
    u: PSemVer;
    i: integer;
  begin
    if obj.findObject('dependencies' + suffix, deps) then
    begin
      {$IFDEF WINDOWS}
      z := GetEnvironmentVariable('APPDATA') + '\dub\packages\';
      {$ELSE}
      z := GetEnvironmentVariable('HOME') + '/.dub/packages/';
      {$ENDIF}
      for i := 0 to deps.Count-1 do
      begin
        n := deps.Names[i];
        s := z + n;

        // Try to fetch if not present at all
        if not fLocalPackages.find(n, pck) and dubBuildOptions.autoFetch then
        begin
          with TProcess.Create(nil) do
          try
            Executable := exeFullName('dub' + exeExt);
            Options := Options + [poUsePipes];
            ShowWindow:= swoHIDE;
            Parameters.Add('fetch');
            Parameters.Add(n);
            Execute;
            if ExitStatus = 0 then
              fLocalPackages.update();
          finally
            free;
          end;
        end;

        if fLocalPackages.find(n, pck) then
        begin

          j := deps.Items[i];
          if j.JSONType <> TJSONtype.jtString then
            continue;

          //split version operator and version number
          v := j.AsString;
          r.init(v);
          o := r.takeUntil(['0'..'9']).yield;
          p := r.takeUntil(#0).yield;
          if p = '*' then
          begin
            o := '>=';
            p := '0.0.0';
          end
          else if (p = 'master') or (v = '~master') then
            q.init('v0.0.0-master')
          else
            q.init('v' + p);

          // Finds a match for the version in the local packages list.
          u := fLocalPackages.find(n, o, q, pck);

          // Try to fetch the right version if no match
          if not assigned(u) and dubBuildOptions.autoFetch then
          begin
            with TProcess.Create(nil) do
            try
              Executable := exeFullName('dub' + exeExt);
              Options := Options + [poUsePipes];
              ShowWindow:= swoHIDE;
              Parameters.Add('fetch');
              Parameters.Add(n);
              Parameters.Add('--version=' + p);
              Execute;
              if ExitStatus = 0 then
              begin
                fLocalPackages.update();
                u := fLocalPackages.find(n, o, q, pck);
              end;
            finally
              free;
            end;
          end;

          // Set the imports, used in particular by DCD
          if assigned(u) then
          begin
            p :=  s + '-' + u^.asString + DirectorySeparator + n + DirectorySeparator;
            if (p + 'source').dirExists then
              fImportPaths.Add(p + 'source')
            else if (p + 'src').dirExists then
              fImportPaths.Add(p + 'src');
          end;
        end;
      end;
    end;
  end;

var
  conf: TJSONObject;
begin
  if fJSON.isNil then
    exit;

  addFrom(fJSON);
  addDepsFrom(fJSON);
  {$IFDEF WINDOWS}
  addDepsFrom(fJSON, '-windows');
  {$ENDIF}
  {$IFDEF LINUX}
  addDepsFrom(fJSON, '-linux');
  {$ENDIF}
  {$IFDEF DARWIN}
  addDepsFrom(fJSON, '-osx');
  {$ENDIF}
  {$IFDEF UNIX}
  addDepsFrom(fJSON, '-posix');
  {$ENDIF}

  conf := getCurrentCustomConfig;
  if conf.isNotNil then
  begin
    addFrom(conf);
    addDepsFrom(conf);
    {$IFDEF WINDOWS}
    addDepsFrom(conf, '-windows');
    {$ENDIF}
    {$IFDEF LINUX}
    addDepsFrom(conf, '-linux');
    {$ENDIF}
    {$IFDEF DARWIN}
    addDepsFrom(conf, '-osx');
    {$ENDIF}
    {$IFDEF UNIX}
    addDepsFrom(conf, '-posix');
    {$ENDIF}
  end;
end;

procedure TCEDubProject.updateOutputNameFromJson;
var
  conf: TJSONObject;
  item: TJSONData;
  namePart, pathPart: string;
  procedure setFrom(obj: TJSONObject);
  var
    n,p: TJSONData;
  begin
    if obj.findAny('targetPath', p) then
      pathPart := p.AsString;
    if obj.FindAny('targetName', n) then
      namePart := n.AsString;
  end;
begin
  fOutputFileName := '';
  if fJSON.isNil or not fJSON.findAny('name', item) then
    exit;

  namePart := item.AsString;
  pathPart := fBasePath;
  setFrom(fJSON);
  conf := getCurrentCustomConfig;
  if conf.isNotNil then
    setFrom(conf);
  pathPart := TrimRightSet(pathPart, ['/','\']);
  {$IFNDEF WINDOWS}
  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}

{$REGION Miscellaneous DUB free functions --------------------------------------}
function sdl2json(const filename: string): TJSONObject;
var
  dub: TProcess;
  str: TStringList;
  jsn: TJSONData;
  prs: TJSONParser;
  old: string;
begin
  result := nil;
  dub := TProcess.Create(nil);
  str := TStringList.Create;
  old := GetCurrentDirUTF8;
  try
    SetCurrentDirUTF8(filename.extractFilePath);
    dub.Executable := 'dub' + exeExt;
    dub.Options := [poUsePipes{$IFDEF WINDOWS}, poNewConsole{$ENDIF}];
    dub.ShowWindow := swoHIDE;
    dub.CurrentDirectory:= filename.extractFilePath;
    dub.Parameters.Add('convert');
    dub.Parameters.Add('-s');
    dub.Parameters.Add('-f');
    dub.Parameters.Add('json');
    dub.Execute;
    processOutputToStrings(dub, str);
    while dub.Running do;
    prs := TJSONParser.Create(str.Text, [joIgnoreTrailingComma, joUTF8]);
    try
      try
        jsn := prs.Parse;
        try
          if jsn.isNotNil then
            result := TJSONObject(jsn.Clone)
          else
            result := nil;
        finally
          jsn.free;
        end;
      finally
        prs.Free
      end;
    except
      result := nil;
    end;
  finally
    SetCurrentDirUTF8(old);
    dub.free;
    str.Free;
  end;
end;

function isValidDubProject(const filename: string): boolean;
var
  maybe: TCEDubProject;
  ext: string;
begin
  ext := filename.extractFileExt.upperCase;
  if (ext <> '.JSON') and (ext <> '.SDL') then
    exit(false);
  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.isNil or maybe.filename.isEmpty then
        result := false
      else if maybe.json.Find('name').isNil then
        result := false;
    except
      result := false;
    end;
  finally
    maybe.Free;
    EntitiesConnector.endUpdate;
  end;
end;

function getDubCompiler: DCompiler;
begin
  exit(DubCompiler);
end;

procedure setDubCompiler(value: DCompiler);
var
  sel: ICECompilerSelector;
begin
  sel := getCompilerSelector;
  DubCompiler := value;
  if not sel.isCompilerValid(DubCompiler) then
    DubCompiler := dmd;
  DubCompilerFilename:=sel.getCompilerPath(DubCompiler);
end;
{$ENDREGION}

initialization
  setDubCompiler(dmd);
  dubBuildOptions:= TCEDubBuildOptions.create(nil);
finalization
  dubBuildOptions.free;
end.

