diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
index 40eecfd06a..2dfd66f4ab 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
+++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
@@ -22,6 +22,7 @@
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
index 0244d46c47..3de12b7fca 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
@@ -34,6 +34,7 @@ namespace Volo.Abp.Cli
options.Commands["switch-to-stable"] = typeof(SwitchToStableCommand);
options.Commands["switch-to-nightly"] = typeof(SwitchToNightlyCommand);
options.Commands["translate"] = typeof(TranslateCommand);
+ options.Commands["build"] = typeof(BuildCommand);
});
}
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs
new file mode 100644
index 0000000000..2a663ed319
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using LibGit2Sharp;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultChangedProjectFinder : IChangedProjectFinder, ITransientDependency
+ {
+ private readonly IRepositoryBuildStatusStore _repositoryBuildStatusStore;
+
+ public DefaultChangedProjectFinder(IRepositoryBuildStatusStore repositoryBuildStatusStore)
+ {
+ _repositoryBuildStatusStore = repositoryBuildStatusStore;
+ }
+
+ public List Find(DotNetProjectBuildConfig buildConfig)
+ {
+ if (buildConfig.SlFilePath.IsNullOrEmpty())
+ {
+ return FindByRepository(buildConfig.GitRepository);
+ }
+
+ return FindBySlnFile(buildConfig.GitRepository, buildConfig.SlFilePath);
+ }
+
+ private List FindByRepository(GitRepository gitRepository)
+ {
+ var changedProjectList = new List();
+ var gitRepositoryBuildStatus = _repositoryBuildStatusStore.Get(gitRepository);
+
+ FindChangedFiles(gitRepository, gitRepositoryBuildStatus, changedProjectList);
+
+ return changedProjectList;
+ }
+
+ private List FindBySlnFile(GitRepository gitRepository, string slnFilePath)
+ {
+ var slFine = new FileInfo(slnFilePath);
+ var changedProjectList = new List();
+ var csProjFiles = slFine.Directory.GetFiles(
+ "*.csproj",
+ SearchOption.AllDirectories
+ ).Select(e => e.FullName)
+ .ToList();
+
+ foreach (var csProjFile in csProjFiles)
+ {
+ AddDependingProjectsToList(gitRepository, csProjFile, changedProjectList);
+ }
+
+ return changedProjectList;
+ }
+
+ private void AddDependingProjectsToList(
+ GitRepository gitRepository,
+ string csProjFilePath,
+ List changedProjectList)
+ {
+ var repositoryName = gitRepository.FindRepositoryOf(csProjFilePath);
+ var project = new DotNetProjectInfo(repositoryName, csProjFilePath);
+ if (changedProjectList.Any(e => e.CsProjPath == csProjFilePath))
+ {
+ return;
+ }
+
+ changedProjectList.Add(project);
+
+ AddProjectDependencies(gitRepository, project, changedProjectList);
+ }
+
+ private void AddProjectDependencies(GitRepository gitRepository, DotNetProjectInfo project, List changedProjectList)
+ {
+ var projectNode = XElement.Load(project.CsProjPath);
+ var referenceNodes = projectNode.Descendants("ItemGroup").Descendants("ProjectReference");
+
+ foreach (var referenceNode in referenceNodes)
+ {
+ if (referenceNode.Attribute("Include") == null)
+ {
+ continue;
+ }
+
+ var relativePath = referenceNode.Attribute("Include").Value;
+ var file = new FileInfo(project.CsProjPath);
+ var referenceProjectInfo = new FileInfo(Path.Combine(file.Directory.FullName, relativePath));
+
+ if (changedProjectList.Any(e => e.CsProjPath == referenceProjectInfo.FullName))
+ {
+ continue;
+ }
+
+ var repositoryName = gitRepository.FindRepositoryOf(referenceProjectInfo.FullName);
+ var referenceProject = new DotNetProjectInfo(repositoryName, referenceProjectInfo.FullName);
+ changedProjectList.Add(referenceProject);
+
+ AddProjectDependencies(gitRepository, referenceProject, changedProjectList);
+ }
+ }
+
+ private void FindChangedFiles(
+ GitRepository repository,
+ GitRepositoryBuildStatus repositoryBuildStatus,
+ List changedProjectList)
+ {
+ if (repositoryBuildStatus.CommitId.IsNullOrEmpty())
+ {
+ AddAllCsProjFiles(repository, changedProjectList);
+ }
+ else
+ {
+ AddChangedCsProjFiles(repository, changedProjectList, repositoryBuildStatus);
+ }
+
+ if (repository.DependingRepositories.Any())
+ {
+ foreach (var dependingRepository in repository.DependingRepositories)
+ {
+ var dependingRepositoryBuildStatus = repositoryBuildStatus.GetChild(dependingRepository.Name);
+ FindChangedFiles(dependingRepository, dependingRepositoryBuildStatus, changedProjectList);
+ }
+ }
+ }
+
+ private void AddAllCsProjFiles(GitRepository repository, List changedFiles)
+ {
+ var allCsProjFiles = Directory.GetFiles(
+ repository.RootPath,
+ "*.csproj",
+ SearchOption.AllDirectories
+ );
+
+ foreach (var file in allCsProjFiles)
+ {
+ changedFiles.Add(
+ new DotNetProjectInfo(repository.Name, Path.Combine(repository.RootPath, file))
+ );
+ }
+ }
+
+ private void AddChangedCsProjFiles(
+ GitRepository repository,
+ List changedFiles,
+ GitRepositoryBuildStatus status)
+ {
+ using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
+ {
+ var firstCommit = status.CommitId.IsNullOrEmpty()
+ ? null
+ : repo.Lookup(status.CommitId);
+ var repoDifferences = repo.Diff.Compare(firstCommit?.Tree, repo.Head.Tip.Tree);
+
+ var files = repoDifferences
+ .Where(e => e.Path.EndsWith(".cs") || e.Path.EndsWith(".csproj"))
+ .Where(e => e.Status != ChangeKind.Deleted)
+ .Select(e => e)
+ .ToList();
+
+ foreach (var file in files)
+ {
+ var csProjPath = Path.Combine(repository.RootPath, file.Path);
+ if (status.SucceedProjects.Any(p => p.CsProjPath == csProjPath && p.CommitId == "1"))
+ {
+ continue;
+ }
+
+ changedFiles.Add(
+ new DotNetProjectInfo(repository.Name, csProjPath)
+ );
+ }
+
+ if (!repository.DependingRepositories.Any())
+ {
+ return;
+ }
+
+ foreach (var subRepository in repository.DependingRepositories)
+ {
+ var subRepositoryBuildStatus = status.GetChild(subRepository.Name);
+ FindChangedFiles(subRepository, subRepositoryBuildStatus, changedFiles);
+ }
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs
new file mode 100644
index 0000000000..eea3d85c9f
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using Volo.Abp.Cli.Utils;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultDotNetProjectBuilder : IDotNetProjectBuilder, ITransientDependency
+ {
+ public List Build(List projects, int maxParallelBuildCount, string arguments)
+ {
+ var builtProjects = new ConcurrentBag();
+
+ try
+ {
+ foreach (var project in projects)
+ {
+ if (builtProjects.Contains(project.CsProjPath))
+ {
+ continue;
+ }
+
+ BuildInternal(project, arguments, builtProjects);
+
+ if (!project.Dependencies.Any())
+ {
+ continue;
+ }
+
+ // TODO: use parallel foreach here.
+ foreach (var projectDependency in project.Dependencies)
+ {
+ if (builtProjects.Contains(project.CsProjPath))
+ {
+ continue;
+ }
+
+ BuildInternal(projectDependency, arguments, builtProjects);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+
+ return builtProjects.ToList();
+ }
+
+ private void BuildInternal(DotNetProjectInfo project, string arguments, ConcurrentBag builtProjects)
+ {
+ Console.WriteLine("Building...: dotnet build " + project.CsProjPath + " " + arguments);
+
+ CmdHelper.RunCmdAndGetOutput(
+ "dotnet build " + project.CsProjPath + " " + arguments,
+ out int buildStatus
+ );
+
+ if (buildStatus == 0)
+ {
+ builtProjects.Add(project.CsProjPath);
+ }
+ else
+ {
+ // TODO: throw exception and stop build process.
+ Console.WriteLine("Build failed for :" + project.CsProjPath);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs
new file mode 100644
index 0000000000..efaa1c6075
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectBuildConfig
+ {
+ public string SlFilePath { get; set; }
+
+ public GitRepository GitRepository { get; set; }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs
new file mode 100644
index 0000000000..10092777c2
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectBuildStatus
+ {
+ public string CsProjPath { get; set; }
+
+ public string CommitId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs
new file mode 100644
index 0000000000..27fea096ea
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectDependencyFiller : IDotNetProjectDependencyFiller, ITransientDependency
+ {
+ public ILogger Logger { get; set; }
+
+ public void Fill(List projects)
+ {
+ foreach (var project in projects)
+ {
+ FillProjectDependencies(project);
+ }
+ }
+
+ private void FillProjectDependencies(DotNetProjectInfo project)
+ {
+ var projectNode = XElement.Load(project.CsProjPath);
+ var referenceNodes = projectNode.Descendants("ItemGroup").Descendants("ProjectReference");
+
+ foreach (var referenceNode in referenceNodes)
+ {
+ if (referenceNode.Attribute("Include") == null)
+ {
+ continue;
+ }
+
+ var relativePath = referenceNode.Attribute("Include").Value;
+ var file = new FileInfo(project.CsProjPath);
+ var referenceProjectInfo = new FileInfo(Path.Combine(file.Directory.FullName, relativePath));
+
+ var referenceProject = new DotNetProjectInfo(project.RepositoryName, referenceProjectInfo.FullName);
+
+ project.Dependencies.Add(referenceProject);
+ }
+ }
+
+ private void GetProjectsOfRepository(GitRepository repository, List projectPaths)
+ {
+ Logger.LogInformation("repo name:" + repository.Name);
+ Logger.LogInformation("directoryPath:" + repository.RootPath);
+
+ GetProjectsOfADirectory(repository.RootPath, projectPaths, new List());
+
+ if (!repository.DependingRepositories.Any())
+ {
+ return;
+ }
+
+ foreach (var dependantRepository in repository.DependingRepositories)
+ {
+ GetProjectsOfRepository(dependantRepository, projectPaths);
+ }
+ }
+
+ private void GetProjectsOfADirectory(
+ string directoryPath,
+ List projectPaths,
+ List ignoredDirectories)
+ {
+ Logger.LogInformation("directoryPath:" + directoryPath);
+
+ var files = Directory.GetFiles(directoryPath, "*.csproj", SearchOption.AllDirectories)
+ .Select(f => f)
+ .ToList();
+
+ // foreach (var ignoredDirectory in ignoredDirectories)
+ // {
+ // files = files.Where(e => !e.StartsWith(Path.Combine(directoryPath, ignoredDirectory)))
+ // .ToList();
+ // }
+
+ projectPaths.AddRange(files);
+
+ // foreach (var projectFile in files)
+ // {
+ // var projectNode = XElement.Load(projectFile);
+ // if (projectNode.Attribute("Sdk") == null)
+ // {
+ // continue;
+ // }
+ //
+ // projectPaths.Add(projectFile);
+ // }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs
new file mode 100644
index 0000000000..0bc431d3a0
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ [Serializable]
+ public class DotNetProjectInfo
+ {
+ public string RepositoryName { get; set; }
+
+ public string CsProjPath { get; set; }
+
+ public List Dependencies { get; set; }
+
+ public DotNetProjectInfo(string repositoryName, string csProjPath)
+ {
+ RepositoryName = repositoryName;
+ CsProjPath = csProjPath;
+ Dependencies = new List();
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs
new file mode 100644
index 0000000000..6af55c8ed2
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ ///
+ /// Represents a source code repository
+ ///
+ public class GitRepository
+ {
+ ///
+ /// Name of the repository
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Branch of the repository
+ ///
+ public string BranchName { get; set; }
+
+ ///
+ /// Root path of the repository which contains .git folder
+ ///
+ public string RootPath { get; set; }
+
+ public List DependingRepositories { get; set; }
+
+ public GitRepository(string name, string branchName, string rootPath)
+ {
+ Name = name;
+ BranchName = branchName;
+ RootPath = rootPath;
+ DependingRepositories = new List();
+ }
+
+ public string GetUniqueName()
+ {
+ var name = Name + "_" + BranchName;
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+
+ return name.ToMd5();
+ }
+
+ private void AddToUniqueName(GitRepository gitRepository, string name)
+ {
+ name += "_" + gitRepository.Name + "_" + gitRepository.BranchName;
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+ }
+
+ public string FindRepositoryOf(string csProjFilePath)
+ {
+ if (csProjFilePath.StartsWith(RootPath))
+ {
+ return Name;
+ }
+
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ var name = FindRepositoryOfInternal(dependingRepository, csProjFilePath);
+ if (!string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+ }
+
+ return null;
+ }
+
+ private string FindRepositoryOfInternal(GitRepository repository, string csProjFilePath)
+ {
+ if (csProjFilePath.StartsWith(repository.RootPath))
+ {
+ return repository.Name;
+ }
+
+ foreach (var dependingRepository in repository.DependingRepositories)
+ {
+ return FindRepositoryOfInternal(dependingRepository, csProjFilePath);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs
new file mode 100644
index 0000000000..751a205b01
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class GitRepositoryBuildStatus
+ {
+ ///
+ /// Name of the repository
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Branch of the repository
+ ///
+ public string BranchName { get; set; }
+
+ ///
+ /// Last succeeded commitId of the repository
+ ///
+ public string CommitId { get; set; }
+
+ ///
+ /// Build succeeded projects of this repository
+ ///
+ public List SucceedProjects { get; set; }
+
+ ///
+ /// Build status of depending repositories
+ ///
+ public List DependingRepositories { get; set; }
+
+ public GitRepositoryBuildStatus(string name, string branchName)
+ {
+ Name = name;
+ BranchName = branchName;
+ SucceedProjects = new List();
+ DependingRepositories = new List();
+ }
+
+ public GitRepositoryBuildStatus GetChild(string repositoryName)
+ {
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ return GetChildInternal(dependingRepository, repositoryName);
+ }
+
+ return null;
+ }
+
+ private GitRepositoryBuildStatus GetChildInternal(GitRepositoryBuildStatus repositoryBuildStatus,
+ string repositoryName)
+ {
+ if (repositoryBuildStatus.Name == repositoryName)
+ {
+ return repositoryBuildStatus;
+ }
+
+ foreach (var dependingRepository in repositoryBuildStatus.DependingRepositories)
+ {
+ return GetChildInternal(dependingRepository, repositoryName);
+ }
+
+ return null;
+ }
+
+ public string GetUniqueName()
+ {
+ var name = Name + "_" + BranchName;
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+
+ return name.ToMd5();
+ }
+
+ private void AddToUniqueName(GitRepositoryBuildStatus gitRepository, string name)
+ {
+ name += "_" + gitRepository.Name + "_" + gitRepository.BranchName;
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildArgsReader.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildArgsReader.cs
new file mode 100644
index 0000000000..7402cd7450
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildArgsReader.cs
@@ -0,0 +1,69 @@
+using System;
+using System.IO;
+using System.Linq;
+using LibGit2Sharp;
+using Volo.Abp.Json;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectBuildConfigReader
+ {
+ DotNetProjectBuildConfig Read(string directoryPath);
+ }
+
+ public class FileSystemDotNetProjectBuildConfigReader : IDotNetProjectBuildConfigReader
+ {
+ private IJsonSerializer _jsonSerializer;
+
+ public FileSystemDotNetProjectBuildConfigReader(IJsonSerializer jsonSerializer)
+ {
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public DotNetProjectBuildConfig Read(string directoryPath)
+ {
+ var buildArgs = new DotNetProjectBuildConfig();
+ var solutionFiles = Directory.GetFiles(directoryPath, "*.sln", SearchOption.TopDirectoryOnly);
+ if (solutionFiles.Length == 1)
+ {
+ buildArgs.SlFilePath = solutionFiles.First();
+ }
+ else
+ {
+ Console.WriteLine(
+ "There are more than 1 solution (*.sln) file in the directory. Searching for build-config.json file."
+ );
+ }
+
+ var configFiles = Directory.GetFiles(directoryPath, "abp-build-config.json", SearchOption.TopDirectoryOnly);
+ if (configFiles.Length == 1)
+ {
+ var configFile = configFiles.First();
+ buildArgs.GitRepository = _jsonSerializer.Deserialize(configFile);
+ }
+ else
+ {
+ Console.WriteLine(
+ "There are more than 1 config (abp-build-config.json) file in the directory!"
+ );
+ }
+
+ SetBranchNames(buildArgs.GitRepository);
+
+ return buildArgs;
+ }
+
+ private void SetBranchNames(GitRepository gitRepository)
+ {
+ using (var repo = new Repository(string.Concat(gitRepository.RootPath, @"\.git")))
+ {
+ gitRepository.BranchName = repo.Head.FriendlyName;
+ }
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ SetBranchNames(dependingRepository);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs
new file mode 100644
index 0000000000..b381a073b4
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IChangedProjectFinder
+ {
+ List Find(DotNetProjectBuildConfig buildConfig);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs
new file mode 100644
index 0000000000..46227bb285
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectBuilder
+ {
+ List Build(List projects, int maxParallelBuildCount, string arguments);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs
new file mode 100644
index 0000000000..cc1ba65bc7
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectDependencyFiller
+ {
+ void Fill(List projects);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs
new file mode 100644
index 0000000000..793d3715f7
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs
@@ -0,0 +1,58 @@
+using System.IO;
+using Newtonsoft.Json;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.IO;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IRepositoryBuildStatusStore
+ {
+ GitRepositoryBuildStatus Get(GitRepository repository);
+
+ void Set(GitRepositoryBuildStatus status);
+ }
+
+ public class FileSystemRepositoryBuildStatusStore : IRepositoryBuildStatusStore, ITransientDependency
+ {
+ // TODO: change this ?
+ private string BaseBuildStatusStorePath = @"C:\Users\ismai\.abp\build";
+
+ public GitRepositoryBuildStatus Get(GitRepository repository)
+ {
+ if (!Directory.Exists(BaseBuildStatusStorePath))
+ {
+ Directory.CreateDirectory(BaseBuildStatusStorePath);
+ }
+
+ var buildStatusFile = Path.Combine(BaseBuildStatusStorePath, repository.GetUniqueName()) + ".json";
+
+ if (!File.Exists(buildStatusFile))
+ {
+ // TODO: this is wrong
+ return new GitRepositoryBuildStatus(repository.Name, repository.BranchName);
+ }
+
+ var buildStatusText = File.ReadAllText(buildStatusFile);
+ return JsonConvert.DeserializeObject(buildStatusText);
+ }
+
+ public void Set(GitRepositoryBuildStatus status)
+ {
+ if (!Directory.Exists(BaseBuildStatusStorePath))
+ {
+ Directory.CreateDirectory(BaseBuildStatusStorePath);
+ }
+
+ var buildStatusFile = Path.Combine(BaseBuildStatusStorePath, status.GetUniqueName()) + ".json";
+ if (File.Exists(buildStatusFile))
+ {
+ FileHelper.DeleteIfExists(buildStatusFile);
+ }
+
+ using (var file = File.CreateText(buildStatusFile))
+ {
+ new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, status);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
new file mode 100644
index 0000000000..56400c7c51
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using LibGit2Sharp;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.Cli.Args;
+using Volo.Abp.Cli.Build;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Commands
+{
+ public class BuildCommand : IConsoleCommand, ITransientDependency
+ {
+ public ILogger Logger { get; set; }
+
+ public IDotNetProjectDependencyFiller DotNetProjectDependencyFiller { get; set; }
+
+ public IChangedProjectFinder ChangedProjectFinder { get; set; }
+
+ public IDotNetProjectBuilder DotNetProjectBuilder { get; set; }
+
+ public IRepositoryBuildStatusStore RepositoryBuildStatusStore { get; set; }
+
+ public IDotNetProjectBuildConfigReader DotNetProjectBuildConfigReader { get; set; }
+
+ public Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ {
+ var sw = new Stopwatch();
+ sw.Start();
+
+ var buildConfig = DotNetProjectBuildConfigReader.Read(Directory.GetCurrentDirectory());
+
+ var maxParallelBuild = commandLineArgs.Options.GetOrNull(
+ Options.MaxParallelBuild.Short,
+ Options.MaxParallelBuild.Long
+ );
+
+ var dotnetBuildArguments = commandLineArgs.Options.GetOrNull(Options.DotnetBuildArguments.Short,
+ Options.DotnetBuildArguments.Long);
+
+ var changedProjectFiles = ChangedProjectFinder.Find(buildConfig);
+
+ DotNetProjectDependencyFiller.Fill(changedProjectFiles);
+
+ var sortedProjects = changedProjectFiles.SortByDependencies(p => p.Dependencies);
+
+ var buildSucceededProjects = DotNetProjectBuilder.Build(
+ sortedProjects,
+ string.IsNullOrEmpty(maxParallelBuild) ? 1 : Convert.ToInt32(maxParallelBuild),
+ dotnetBuildArguments ?? ""
+ );
+
+ var buildStatus = GenerateBuildStatus(buildConfig.GitRepository, changedProjectFiles, buildSucceededProjects);
+ RepositoryBuildStatusStore.Set(buildStatus);
+
+ sw.Stop();
+ Console.WriteLine("Build operation is completed in " + sw.ElapsedMilliseconds + " (ms)");
+
+ return Task.CompletedTask;
+ }
+
+ private GitRepositoryBuildStatus GenerateBuildStatus(
+ GitRepository gitRepository,
+ List changedProjects,
+ List buildSucceededProjects)
+ {
+ using (var repo = new Repository(string.Concat(gitRepository.RootPath, @"\.git")))
+ {
+ var lastCommitId = repo.Head.Tip.Id.ToString();
+ var status = new GitRepositoryBuildStatus(
+ gitRepository.Name,
+ repo.Head.FriendlyName
+ )
+ {
+ CommitId = lastCommitId
+ };
+
+ status.SucceedProjects = changedProjects.Where(p =>
+ p.RepositoryName == gitRepository.Name &&
+ buildSucceededProjects.Contains(p.CsProjPath)
+ )
+ .Select(e => new DotNetProjectBuildStatus()
+ {
+ CsProjPath = e.CsProjPath,
+ CommitId = lastCommitId
+ }).ToList();
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ GenerateBuildStatusInternal(dependingRepository, changedProjects, buildSucceededProjects, status);
+ }
+
+ return status;
+ }
+ }
+
+ private void GenerateBuildStatusInternal(
+ GitRepository gitRepository,
+ List changedProjects,
+ List buildSucceededProjects,
+ GitRepositoryBuildStatus status)
+ {
+ using (var repo = new Repository(string.Concat(gitRepository.RootPath, @"\.git")))
+ {
+ var lastCommitId = repo.Head.Tip.Id.ToString();
+ var dependingRepositoryStatus = new GitRepositoryBuildStatus(
+ gitRepository.Name,
+ repo.Head.FriendlyName
+ )
+ {
+ CommitId = lastCommitId
+ };
+
+ dependingRepositoryStatus.SucceedProjects = changedProjects.Where(p =>
+ p.RepositoryName == gitRepository.Name &&
+ buildSucceededProjects.Contains(p.CsProjPath)
+ )
+ .Select(e => new DotNetProjectBuildStatus()
+ {
+ CsProjPath = e.CsProjPath,
+ CommitId = lastCommitId
+ }).ToList();
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ GenerateBuildStatusInternal(
+ dependingRepository,
+ changedProjects,
+ buildSucceededProjects,
+ dependingRepositoryStatus
+ );
+ }
+
+ status.DependingRepositories.Add(dependingRepositoryStatus);
+ }
+ }
+
+ public string GetUsageInfo()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("");
+ sb.AppendLine("Usage:");
+ sb.AppendLine("");
+ sb.AppendLine(" abp build [options]");
+ sb.AppendLine("");
+ sb.AppendLine("Options:");
+ sb.AppendLine("");
+ // TODO: Explain extra parameters
+ sb.AppendLine("");
+ sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
+
+ return sb.ToString();
+ }
+
+ public string GetShortDescription()
+ {
+ return "Builds a dotnet repository and dependent repositories or a solution.";
+ }
+
+ public static class Options
+ {
+ public static class MaxParallelBuild
+ {
+ public const string Short = "m";
+ public const string Long = "max-paralel-builds";
+ }
+
+ public static class DotnetBuildArguments
+ {
+ public const string Short = "d";
+ public const string Long = "dotnet-build-arguments";
+ }
+ }
+ }
+}