Browse Source

Merge pull request #5418 from abpframework/feat/build-command

Introduced "abp build" command to optimize building for big mono repositories
pull/5443/head
İsmail ÇAĞDAŞ 6 years ago
committed by GitHub
parent
commit
0859b4dae3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 67
      docs/en/CLI-BuildCommand.md
  3. 29
      docs/en/CLI.md
  4. 1
      framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
  5. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  6. 67
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs
  7. 122
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs
  8. 352
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs
  9. 76
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs
  10. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs
  11. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs
  12. 41
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs
  13. 25
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs
  14. 17
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs
  15. 47
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs
  16. 93
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs
  17. 61
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs
  18. 94
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs
  19. 143
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs
  20. 24
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs
  21. 11
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs
  22. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs
  23. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs
  24. 7
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs
  25. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs
  26. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs
  27. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs
  28. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs
  29. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
  30. 132
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
  31. 8
      framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs
  32. 79
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildProjectListSorter_Tests.cs
  33. 123
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildStatusGenerator_Tests.cs
  34. 318
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepositoryBuildStatus_Tests.cs
  35. 52
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepository_Tests.cs

2
.gitignore

@ -303,4 +303,4 @@ modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/Logs/
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json
/templates/app/angular/package-lock.json
/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json
abp-build-config.json

67
docs/en/CLI-BuildCommand.md

@ -0,0 +1,67 @@
# Build Command
Building a .NET project is hard when the project references a project reference outside of the solution or even from a different GIT repository. This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of;
* A .NET solution file (*.sln)
* abp-build-config.json
When the executing directory (or ```--working-directory``` parameter's directory) contains a .NET solution file, ```build``` command builds all the projects in the related solution file and all project references on building project files recursively.
When the executing directory (or ```--working-directory``` parameter's directory) contains a ```abp-build-config.json```, ```build``` command builds all changed projects form its last build and all project references on building project files recursively.
# Build Command Config
```abp-build-config.json``` contains properties below;
* ```Name```: Name of the GIT repository. This can be friendly name of your GIT repository or any other unique string for the repository.
* ```RootPath```: Root path of the repository which contains ```.git``` folder.
* ```DependingRepositories```: Depending repository list of a repository. Each depending repository item contains same fields as a repository.
* ```IgnoredDirectories```: Relative directory paths to ignore while building a GIT repository.
A sample ```abp-build-config.json``` looks like for a Windows OS;
````json
{
"Name": "main-repository",
"RootPath": "D:\\GitHub\\main-repository",
"DependingRepositories": [{
"Name": "module-repository",
"RootPath": "D:\\GitHub\\module-repository"
}],
"IgnoredDirectories": [
"utils"
]
}
````
# Build Status
ABP CLI stores a build status file for builds using the repository friendly names and current branch names in the;
* ```%USERPROFILE%\.abp\build\``` for Windows.
* ```$HOME/.abp/build/``` for Linux/macOS.
and uses this file when building same repository next time and only builds affected projects and decreases the total build time. A sample build status file content looks like;
````json
{
"RepositoryName": "main-repository",
"BranchName": "dev",
"CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba",
"SucceedProjects": [{
"CsProjPath": "D:\\GitHub\\main-repository\\BookStore\\BookStore.Web.csproj",
"CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba"
}],
"DependingRepositories": [{
"RepositoryName": "module-repository",
"BranchName": "dev",
"CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04",
"SucceedProjects": [{
"CsProjPath": "D:\\GitHub\\module-repository\\identity-module\Identity\\Identity.Web.csproj",
"CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04"
}],
"DependingRepositories": []
}]
}
````

29
docs/en/CLI.md

@ -40,6 +40,7 @@ Here, the list of all available commands before explaining their details:
* **`translate`**: Simplifies to translate localization files when you have multiple JSON [localization](Localization.md) files in a source control repository.
* **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password.
* **`logout`**: Logouts from your computer if you've authenticated before.
* **`build`**: Builds a GIT repository and depending repositories or a single .NET solution.
### help
@ -360,3 +361,31 @@ Logs you out by removing the session token from your computer.
abp logout
```
### build
This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of;
* A .NET solution file (*.sln)
* abp-build-config.json (suggested to add this to .gitignore)
Usage:
````bash
abp build [options]
````
Example:
```
abp build --build-name "prod" --dotnet-build-arguments "\"--no-dependencies\""
```
#### Options
* ```--working-directory``` or ```-w```: Specifies the working directory. This option is useful when the command is executed outside of a GIT repository or when executing directory doesn't contain a .NET solution file.
* ```--build-name``` or ```-n```: Specifies a name for the build. This option is useful when same repository is used for more than one different builds.
* ```--dotnet-build-arguments``` or ```-a```: Arguments to pass ```dotnet build``` when building project files. This parameter must be passed like ```"\"{params}\""``` .
* ```--force``` or ```-f```: Forces to build projects even they are not changed from the last successful build.
For more details, see [build command documentation](CLI-BuildCommand.md).

1
framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj

@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Polly" Version="7.2.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="LibGit2Sharp" Version="0.26.2" />
</ItemGroup>
<ItemGroup>

1
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs

@ -36,6 +36,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);
});
}
}

67
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Build
{
public class DefaultBuildProjectListSorter : IBuildProjectListSorter, ITransientDependency
{
public List<DotNetProjectInfo> SortByDependencies(
List<DotNetProjectInfo> source,
IEqualityComparer<DotNetProjectInfo> comparer = null)
{
/* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp
* http://en.wikipedia.org/wiki/Topological_sorting
*/
var sorted = new List<DotNetProjectInfo>();
var visited = new Dictionary<DotNetProjectInfo, bool>(comparer);
foreach (var item in source)
{
SortByDependenciesVisit(source, item, sorted, visited);
}
return sorted;
}
private void SortByDependenciesVisit(
List<DotNetProjectInfo> source,
DotNetProjectInfo item,
List<DotNetProjectInfo> sorted,
Dictionary<DotNetProjectInfo, bool> visited)
{
bool inProcess;
var alreadyVisited = visited.TryGetValue(item, out inProcess);
if (alreadyVisited)
{
if (inProcess)
{
throw new ArgumentException("Cyclic dependency found! Item: " + item);
}
}
else
{
visited[item] = true;
var dependencies = item.Dependencies;
if (dependencies != null)
{
foreach (var dependency in dependencies)
{
var dependencyItem = source.FirstOrDefault(e => e.CsProjPath == dependency.CsProjPath);
if (dependencyItem != null)
{
SortByDependenciesVisit(source, dependencyItem, sorted, visited);
}
}
}
visited[item] = false;
sorted.Add(item);
}
}
}
}

122
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LibGit2Sharp;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Build
{
public class DefaultBuildStatusGenerator : IBuildStatusGenerator, ITransientDependency
{
private readonly IGitRepositoryHelper _gitRepositoryHelper;
public DefaultBuildStatusGenerator(IGitRepositoryHelper gitRepositoryHelper)
{
_gitRepositoryHelper = gitRepositoryHelper;
}
public GitRepositoryBuildStatus Generate(
DotNetProjectBuildConfig buildConfig,
List<DotNetProjectInfo> changedProjects,
List<string> buildSucceededProjects)
{
var lastCommitId = _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository);
var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository);
var status = new GitRepositoryBuildStatus(
buildConfig.GitRepository.Name,
repoFriendlyName
);
if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects))
{
status.CommitId = lastCommitId;
}
status.SucceedProjects = changedProjects.Where(p =>
p.RepositoryName == buildConfig.GitRepository.Name &&
buildSucceededProjects.Contains(p.CsProjPath)
)
.Select(e => new DotNetProjectBuildStatus
{
CsProjPath = e.CsProjPath,
CommitId = lastCommitId
}).ToList();
foreach (var dependingRepository in buildConfig.GitRepository.DependingRepositories)
{
GenerateBuildStatusInternal(
buildConfig,
dependingRepository,
changedProjects,
buildSucceededProjects,
status
);
}
return status;
}
private bool ShouldUpdateRepositoryCommitId(
DotNetProjectBuildConfig buildConfig,
List<DotNetProjectInfo> changedProjects,
List<string> buildSucceededProjects)
{
if (!buildConfig.SlFilePath.IsNullOrEmpty())
{
return false;
}
if (changedProjects.Count == 0 || buildSucceededProjects.Count == 0)
{
return false;
}
return changedProjects.Count == buildSucceededProjects.Count;
}
private void GenerateBuildStatusInternal(
DotNetProjectBuildConfig buildConfig,
GitRepository gitRepository,
List<DotNetProjectInfo> changedProjects,
List<string> buildSucceededProjects,
GitRepositoryBuildStatus status)
{
var lastCommitId = _gitRepositoryHelper.GetLastCommitId(gitRepository);
var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(gitRepository);
var dependingRepositoryStatus = new GitRepositoryBuildStatus(
gitRepository.Name,
repoFriendlyName
);
if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects))
{
dependingRepositoryStatus.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(
buildConfig,
dependingRepository,
changedProjects,
buildSucceededProjects,
dependingRepositoryStatus
);
}
status.DependingRepositories.Add(dependingRepositoryStatus);
}
}
}

352
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs

@ -0,0 +1,352 @@
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;
private readonly IGitRepositoryHelper _gitRepositoryHelper;
private readonly IDotNetProjectDependencyFiller _dotNetProjectDependencyFiller;
private readonly IBuildProjectListSorter _buildProjectListSorter;
private readonly List<string> _changeDetectionFileExtensions = new List<string>
{
".cs",
".csproj",
".cshtml"
};
public DefaultChangedProjectFinder(
IRepositoryBuildStatusStore repositoryBuildStatusStore,
IGitRepositoryHelper gitRepositoryHelper,
IDotNetProjectDependencyFiller dotNetProjectDependencyFiller,
IBuildProjectListSorter buildProjectListSorter)
{
_repositoryBuildStatusStore = repositoryBuildStatusStore;
_gitRepositoryHelper = gitRepositoryHelper;
_dotNetProjectDependencyFiller = dotNetProjectDependencyFiller;
_buildProjectListSorter = buildProjectListSorter;
}
public List<DotNetProjectInfo> Find(DotNetProjectBuildConfig buildConfig)
{
if (!buildConfig.SlFilePath.IsNullOrEmpty())
{
return FindBySlnFile(buildConfig);
}
return FindByRepository(buildConfig);
}
/// <summary>
/// Returns list of projects in a repository and its depending repositories sorted by dependencies
/// </summary>
/// <param name="buildConfig"></param>
/// <returns></returns>
private List<DotNetProjectInfo> FindAllProjects(DotNetProjectBuildConfig buildConfig)
{
var projects = new List<DotNetProjectInfo>();
AddProjectsOfRepository(buildConfig.GitRepository, projects);
_dotNetProjectDependencyFiller.Fill(projects);
var allSortedProjectList = _buildProjectListSorter.SortByDependencies(
projects,
new DotNetProjectInfoEqualityComparer()
);
FilterIgnoredDirectories(allSortedProjectList, buildConfig.GitRepository);
return allSortedProjectList;
}
private void FilterIgnoredDirectories(List<DotNetProjectInfo> projects, GitRepository gitRepository)
{
foreach (var ignoredDirectory in gitRepository.IgnoredDirectories)
{
projects = projects.Where(e =>
!e.CsProjPath.StartsWith(Path.Combine(gitRepository.RootPath, ignoredDirectory)))
.ToList();
}
foreach (var dependingRepository in gitRepository.DependingRepositories)
{
FilterIgnoredDirectories(projects, dependingRepository);
}
}
private void AddProjectsOfRepository(GitRepository gitRepository, List<DotNetProjectInfo> projects)
{
var allCsProjFiles = Directory.GetFiles(
gitRepository.RootPath,
"*.csproj",
SearchOption.AllDirectories
).ToList();
projects.AddRange(
allCsProjFiles.Select(csProjPath => new DotNetProjectInfo(gitRepository.Name, csProjPath, false))
);
foreach (var dependingRepository in gitRepository.DependingRepositories)
{
AddProjectsOfRepository(dependingRepository, projects);
}
}
private List<DotNetProjectInfo> FindByRepository(DotNetProjectBuildConfig buildConfig)
{
var gitRepositoryBuildStatus = _repositoryBuildStatusStore.Get(
buildConfig.BuildName,
buildConfig.GitRepository
);
var allSortedProjectList = FindAllProjects(buildConfig);
MarkProjectsForBuild(
buildConfig.GitRepository,
gitRepositoryBuildStatus,
buildConfig.ForceBuild,
allSortedProjectList
);
return allSortedProjectList.Where(e => e.ShouldBuild).ToList();
}
private void MarkProjectsForBuild(
GitRepository repository,
GitRepositoryBuildStatus repositoryBuildStatus,
bool forceBuild,
List<DotNetProjectInfo> allProjectList)
{
if (forceBuild || repositoryBuildStatus == null || repositoryBuildStatus.CommitId.IsNullOrEmpty())
{
// Mark all projects for build
allProjectList.ForEach(e => e.ShouldBuild = true);
}
else
{
MarkChangedProjectsForBuild(
repository,
repositoryBuildStatus,
allProjectList
);
}
if (!repository.DependingRepositories.Any())
{
return;
}
foreach (var dependingRepository in repository.DependingRepositories)
{
var dependingRepositoryBuildStatus = repositoryBuildStatus?.GetChild(dependingRepository.Name);
MarkProjectsForBuild(
dependingRepository,
dependingRepositoryBuildStatus,
forceBuild,
allProjectList
);
}
}
private void MarkChangedProjectsForBuild(
GitRepository repository,
GitRepositoryBuildStatus status,
List<DotNetProjectInfo> allProjectList)
{
using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
{
var firstCommit = status.CommitId.IsNullOrEmpty()
? null
: repo.Lookup<Commit>(status.CommitId);
var repoDifferences = repo.Diff.Compare<Patch>(firstCommit?.Tree, repo.Head.Tip.Tree);
var fileExtensionPredicate = PredicateBuilder.New<PatchEntryChanges>(true);
foreach (var changeDetectionFileExtension in _changeDetectionFileExtensions)
{
fileExtensionPredicate = fileExtensionPredicate.Or(
e => e.Path.EndsWith(changeDetectionFileExtension)
);
}
var files = repoDifferences
.Where(fileExtensionPredicate)
.Where(e => e.Status != ChangeKind.Deleted)
.Select(e => e)
.ToList();
var affectedCsProjFiles = FindAffectedCsProjFiles(repository.RootPath, files);
var lastCommitId = _gitRepositoryHelper.GetLastCommitId(repository);
foreach (var file in affectedCsProjFiles)
{
var csProjPath = Path.Combine(repository.RootPath, file);
if (status.SucceedProjects.Any(p => p.CsProjPath == csProjPath && p.CommitId == lastCommitId))
{
continue;
}
// Filter ignored directories
var isIgnored = repository.IgnoredDirectories.Any(ignoredDirectory =>
csProjPath.StartsWith(Path.Combine(repository.RootPath, ignoredDirectory)));
if (isIgnored)
{
continue;
}
allProjectList.MarkForBuild(repository.Name, csProjPath);
AddDependingProjectsToList(repository.Name, csProjPath, allProjectList);
}
}
}
private List<DotNetProjectInfo> FindBySlnFile(DotNetProjectBuildConfig buildConfig)
{
var allProjectList = FindAllProjects(buildConfig);
var slFine = new FileInfo(buildConfig.SlFilePath);
var csProjFiles = slFine.Directory.GetFiles(
"*.csproj",
SearchOption.AllDirectories
).Select(e => e.FullName)
.ToList();
foreach (var csProjFile in csProjFiles)
{
MarkDependantProjectsForBuild(buildConfig.GitRepository, csProjFile, allProjectList);
}
return _buildProjectListSorter.SortByDependencies(
allProjectList,
new DotNetProjectInfoEqualityComparer()
).Where(e => e.ShouldBuild).ToList();
}
private void MarkDependantProjectsForBuild(
GitRepository gitRepository,
string csProjFilePath,
List<DotNetProjectInfo> allProjectList)
{
var repositoryName = gitRepository.FindRepositoryOf(csProjFilePath);
var project = new DotNetProjectInfo(repositoryName, csProjFilePath, true);
if (allProjectList.IsMarkedForBuild(repositoryName, csProjFilePath))
{
return;
}
allProjectList.MarkForBuild(project);
AddProjectDependencies(gitRepository, project, allProjectList);
}
private void AddProjectDependencies(
GitRepository gitRepository,
DotNetProjectInfo project,
List<DotNetProjectInfo> allProjectList)
{
var projectInfo = allProjectList.FirstOrDefault(e => e.CsProjPath == project.CsProjPath);
if (projectInfo == null)
{
return;
}
var dependencies = projectInfo.Dependencies;
foreach (var dependency in dependencies)
{
if (allProjectList.IsMarkedForBuild(dependency.RepositoryName, dependency.CsProjPath))
{
continue;
}
allProjectList.MarkForBuild(dependency.RepositoryName, dependency.CsProjPath);
AddProjectDependencies(gitRepository, dependency, allProjectList);
}
}
private void AddDependingProjectsToList(
string repositoryName,
string csProjPath,
List<DotNetProjectInfo> allProjectList)
{
var dependingProjects = allProjectList.Where(
e => e.Dependencies.Any(d => d.RepositoryName == repositoryName && d.CsProjPath == csProjPath)
).Select(e => new DotNetProjectInfo(e.RepositoryName, e.CsProjPath, true)).ToList();
if (!dependingProjects.Any())
{
return;
}
foreach (var dependingProject in dependingProjects)
{
if (allProjectList.IsMarkedForBuild(dependingProject))
{
continue;
}
allProjectList.MarkForBuild(dependingProject);
AddDependingProjectsToList(dependingProject.RepositoryName, dependingProject.CsProjPath,
allProjectList);
}
}
private List<string> FindAffectedCsProjFiles(string repositoryPath, List<PatchEntryChanges> files)
{
var affectedProjectFiles = new List<string>();
foreach (var file in files)
{
var filePath = Path.Combine(repositoryPath, file.Path);
if (filePath.EndsWith(".csproj"))
{
affectedProjectFiles.Add(filePath);
}
if (!filePath.EndsWith(".cs"))
{
continue;
}
var classFile = new FileInfo(filePath);
var csProjPath = FindBelongingProjectPathOfClass(classFile.Directory?.FullName);
if (csProjPath.IsNullOrEmpty() || affectedProjectFiles.Contains(csProjPath))
{
continue;
}
affectedProjectFiles.Add(csProjPath);
}
return affectedProjectFiles;
}
private string FindBelongingProjectPathOfClass(string directoryPath)
{
var files = Directory.GetFiles(directoryPath, "*.csproj", SearchOption.TopDirectoryOnly);
if (files.Length == 1)
{
return files.First();
}
var directoryInfo = new DirectoryInfo(directoryPath);
if (directoryInfo.Parent == null)
{
return null;
}
return FindBelongingProjectPathOfClass(directoryInfo.Parent.FullName);
}
}
}

76
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs

@ -0,0 +1,76 @@
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<string> Build(List<DotNetProjectInfo> projects, string arguments)
{
var builtProjects = new ConcurrentBag<string>();
var totalProjectCountToBuild = projects.Count;
var buildingProjectIndex = 0;
try
{
foreach (var project in projects)
{
if (builtProjects.Contains(project.CsProjPath))
{
continue;
}
buildingProjectIndex++;
Console.WriteLine(
"Building....: " + " (" + buildingProjectIndex + "/" +
totalProjectCountToBuild + ")" + project.CsProjPath
);
BuildInternal(project, arguments, builtProjects);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
return builtProjects.ToList();
}
private void BuildInternal(DotNetProjectInfo project, string arguments, ConcurrentBag<string> builtProjects)
{
var buildArguments = arguments.TrimStart('"').TrimEnd('"');
Console.WriteLine("Executing...: dotnet build " + project.CsProjPath + " " + buildArguments);
var output = CmdHelper.RunCmdAndGetOutput(
"dotnet build " + project.CsProjPath + " " + buildArguments,
out int buildStatus
);
if (buildStatus == 0)
{
builtProjects.Add(project.CsProjPath);
WriteOutput(output, ConsoleColor.Green);
}
else
{
WriteOutput(output, ConsoleColor.Red);
Console.WriteLine("Build failed for :" + project.CsProjPath);
throw new Exception("Build failed!");
}
}
private void WriteOutput(string text, ConsoleColor color)
{
var currentConsoleColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ForegroundColor = currentConsoleColor;
}
}
}

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs

@ -0,0 +1,13 @@
namespace Volo.Abp.Cli.Build
{
public class DotNetProjectBuildConfig
{
public string BuildName { get; set; }
public string SlFilePath { get; set; }
public GitRepository GitRepository { get; set; }
public bool ForceBuild { get; set; }
}
}

9
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; }
}
}

41
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs

@ -0,0 +1,41 @@
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 void Fill(List<DotNetProjectInfo> 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, false);
project.Dependencies.Add(referenceProject);
}
}
}
}

25
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs

@ -0,0 +1,25 @@
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 bool ShouldBuild { get; set; }
public List<DotNetProjectInfo> Dependencies { get; set; }
public DotNetProjectInfo(string repositoryName, string csProjPath, bool shouldBuild)
{
RepositoryName = repositoryName;
CsProjPath = csProjPath;
ShouldBuild = shouldBuild;
Dependencies = new List<DotNetProjectInfo>();
}
}
}

17
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Volo.Abp.Cli.Build
{
public class DotNetProjectInfoEqualityComparer : EqualityComparer<DotNetProjectInfo>
{
public override bool Equals(DotNetProjectInfo x, DotNetProjectInfo y)
{
return (x == null && y == null) || (x != null && y != null && x.CsProjPath == y.CsProjPath);
}
public override int GetHashCode(DotNetProjectInfo obj)
{
return obj == null ? 0 : obj.CsProjPath.GetHashCode();
}
}
}

47
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs

@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.Cli.Build
{
public static class DotNetProjectInfoExtensions
{
public static void MarkForBuild(this List<DotNetProjectInfo> projects, string repositoryName, string csProjPath)
{
var project = projects.FirstOrDefault(e =>
e.RepositoryName == repositoryName && e.CsProjPath == csProjPath
);
if (project == null)
{
return;
}
project.ShouldBuild = true;
}
public static void MarkForBuild(this List<DotNetProjectInfo> projects, DotNetProjectInfo project)
{
projects.MarkForBuild(project.RepositoryName, project.CsProjPath);
}
public static bool IsMarkedForBuild(this List<DotNetProjectInfo> projects, string repositoryName,
string csProjPath)
{
var project = projects.FirstOrDefault(e =>
e.RepositoryName == repositoryName && e.CsProjPath == csProjPath
);
if (project == null)
{
return false;
}
return project.ShouldBuild;
}
public static bool IsMarkedForBuild(this List<DotNetProjectInfo> projects, DotNetProjectInfo project)
{
return projects.IsMarkedForBuild(project.RepositoryName, project.CsProjPath);
}
}
}

93
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs

@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.Cli.Build
{
public class FileSystemDotNetProjectBuildConfigReader : IDotNetProjectBuildConfigReader, ITransientDependency
{
private readonly IJsonSerializer _jsonSerializer;
private string _buildConfigName = "abp-build-config.json";
public FileSystemDotNetProjectBuildConfigReader(IJsonSerializer jsonSerializer)
{
_jsonSerializer = jsonSerializer;
}
public DotNetProjectBuildConfig Read(string directoryPath)
{
var buildConfig = new DotNetProjectBuildConfig();
var solutionFiles = Directory.GetFiles(directoryPath, "*.sln", SearchOption.TopDirectoryOnly);
if (solutionFiles.Length == 1)
{
buildConfig.SlFilePath = solutionFiles.First();
var configFile = GetClosestFile(directoryPath, _buildConfigName);
var configFileContent = File.ReadAllText(configFile);
buildConfig.GitRepository = _jsonSerializer.Deserialize<GitRepository>(configFileContent);
SetBranchNames(buildConfig.GitRepository);
return buildConfig;
}
var configFiles = Directory.GetFiles(directoryPath, _buildConfigName, SearchOption.TopDirectoryOnly);
if (configFiles.Length == 1)
{
var configFile = configFiles.First();
var configFileContent = File.ReadAllText(configFile);
buildConfig.GitRepository = _jsonSerializer.Deserialize<GitRepository>(configFileContent);
SetBranchNames(buildConfig.GitRepository);
return buildConfig;
}
Console.WriteLine(
"There are more than 1 config (abp-build-config.json) file in the directory!"
);
throw new Exception("There is no solution file (*.sln) or " + _buildConfigName + " in the working directory !");
}
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);
}
}
private string GetClosestFile(string directoryPath, string fileName)
{
var directory = new DirectoryInfo(directoryPath);
var files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly);
if (files.Any() && files.Length == 1)
{
return files.First().FullName;
}
do
{
directory = directory.Parent;
if (directory == null)
{
return string.Empty;
}
files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly);
if (files.Any() && files.Length == 1)
{
return files.First().FullName;
}
} while (true);
}
}
}

61
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs

@ -0,0 +1,61 @@
using System.IO;
using Newtonsoft.Json;
using Volo.Abp.DependencyInjection;
using Volo.Abp.IO;
namespace Volo.Abp.Cli.Build
{
public class FileSystemRepositoryBuildStatusStore : IRepositoryBuildStatusStore, ITransientDependency
{
public GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository)
{
if (!Directory.Exists(CliPaths.Build))
{
Directory.CreateDirectory(CliPaths.Build);
}
var buildStatusFile = Path.Combine(CliPaths.Build, repository.GetUniqueName(buildNamePrefix)) +
".json";
if (!File.Exists(buildStatusFile))
{
return null;
}
var buildStatusText = File.ReadAllText(buildStatusFile);
return JsonConvert.DeserializeObject<GitRepositoryBuildStatus>(buildStatusText);
}
public void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status)
{
var existingRepositoryStatus = Get(buildNamePrefix, repository);
var buildStatusFile = Path.Combine(
CliPaths.Build,
status.GetUniqueName(buildNamePrefix)
) + ".json";
if (File.Exists(buildStatusFile))
{
FileHelper.DeleteIfExists(buildStatusFile);
}
if (existingRepositoryStatus != null)
{
existingRepositoryStatus.MergeWith(status);
using (var file = File.CreateText(buildStatusFile))
{
new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, existingRepositoryStatus);
}
}
else
{
using (var file = File.CreateText(buildStatusFile))
{
new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, status);
}
}
}
}
}

94
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Cli.Build
{
/// <summary>
/// Represents a source code repository
/// </summary>
public class GitRepository
{
/// <summary>
/// Name of the repository
/// </summary>
public string Name { get; set; }
/// <summary>
/// Branch of the repository
/// </summary>
public string BranchName { get; set; }
/// <summary>
/// Root path of the repository which contains .git folder
/// </summary>
public string RootPath { get; set; }
public List<GitRepository> DependingRepositories { get; set; }
public List<string> IgnoredDirectories { get; set; }
public GitRepository(string name, string branchName, string rootPath)
{
Name = name;
BranchName = branchName;
RootPath = rootPath;
DependingRepositories = new List<GitRepository>();
IgnoredDirectories = new List<string>();
}
public string GetUniqueName(string prefix)
{
var name = Name + "_" + BranchName;
foreach (var dependingRepository in DependingRepositories)
{
AddToUniqueName(dependingRepository, name);
}
return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + 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;
}
}
}

143
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.Cli.Build
{
public class GitRepositoryBuildStatus
{
/// <summary>
/// Name of the repository
/// </summary>
public string RepositoryName { get; set; }
/// <summary>
/// Branch of the repository
/// </summary>
public string BranchName { get; set; }
/// <summary>
/// Last succeeded commitId of the repository
/// </summary>
public string CommitId { get; set; }
/// <summary>
/// Build succeeded projects of this repository
/// </summary>
public List<DotNetProjectBuildStatus> SucceedProjects { get; set; }
/// <summary>
/// Build status of depending repositories
/// </summary>
public List<GitRepositoryBuildStatus> DependingRepositories { get; set; }
public GitRepositoryBuildStatus(string repositoryName, string branchName)
{
RepositoryName = repositoryName;
BranchName = branchName;
SucceedProjects = new List<DotNetProjectBuildStatus>();
DependingRepositories = new List<GitRepositoryBuildStatus>();
}
public GitRepositoryBuildStatus GetSelfOrChild(string repositoryName)
{
if (RepositoryName == repositoryName)
{
return this;
}
return GetChild(repositoryName);
}
public GitRepositoryBuildStatus GetChild(string repositoryName)
{
foreach (var dependingRepository in DependingRepositories)
{
var child = GetChildInternal(dependingRepository, repositoryName);
if (child != null)
{
return child;
}
}
return null;
}
public string GetUniqueName(string prefix)
{
var name = RepositoryName + "_" + BranchName;
foreach (var dependingRepository in DependingRepositories)
{
AddToUniqueName(dependingRepository, name);
}
return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + name.ToMd5();
}
public void AddOrUpdateProjectStatus(DotNetProjectBuildStatus status)
{
var existingProjectStatus = SucceedProjects.FirstOrDefault(p => p.CsProjPath == status.CsProjPath);
if (existingProjectStatus != null)
{
existingProjectStatus.CommitId = status.CommitId;
}
else
{
SucceedProjects.Add(status);
}
}
public void MergeWith(GitRepositoryBuildStatus newBuildStatus)
{
if (!newBuildStatus.CommitId.IsNullOrEmpty())
{
CommitId = newBuildStatus.CommitId;
}
foreach (var succeedProject in newBuildStatus.SucceedProjects)
{
AddOrUpdateProjectStatus(succeedProject);
}
foreach (var dependingRepositoryBuildStatus in newBuildStatus.DependingRepositories)
{
var existingDependingRepositoryBuildStatus = GetChild(dependingRepositoryBuildStatus.RepositoryName);
var newDependingRepositoryBuildStatus = newBuildStatus.GetChild(
dependingRepositoryBuildStatus.RepositoryName
);
existingDependingRepositoryBuildStatus.MergeWith(newDependingRepositoryBuildStatus);
}
}
private GitRepositoryBuildStatus GetChildInternal(GitRepositoryBuildStatus repositoryBuildStatus,
string repositoryName)
{
if (repositoryBuildStatus.RepositoryName == repositoryName)
{
return repositoryBuildStatus;
}
foreach (var dependingRepository in repositoryBuildStatus.DependingRepositories)
{
var child = GetChildInternal(dependingRepository, repositoryName);
if (child != null)
{
return child;
}
}
return null;
}
private void AddToUniqueName(GitRepositoryBuildStatus gitRepository, string name)
{
name += "_" + gitRepository.RepositoryName + "_" + gitRepository.BranchName;
foreach (var dependingRepository in gitRepository.DependingRepositories)
{
AddToUniqueName(dependingRepository, name);
}
}
}
}

24
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs

@ -0,0 +1,24 @@
using LibGit2Sharp;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Build
{
public class GitRepositoryHelper : IGitRepositoryHelper, ITransientDependency
{
public string GetLastCommitId(GitRepository repository)
{
using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
{
return repo.Head.Tip.Id.ToString();
}
}
public string GetFriendlyName(GitRepository repository)
{
using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
{
return repo.Head.FriendlyName;
}
}
}
}

11
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Volo.Abp.Cli.Build
{
public interface IBuildProjectListSorter
{
List<DotNetProjectInfo> SortByDependencies(
List<DotNetProjectInfo> source,
IEqualityComparer<DotNetProjectInfo> comparer = null);
}
}

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Volo.Abp.Cli.Build
{
public interface IBuildStatusGenerator
{
GitRepositoryBuildStatus Generate(
DotNetProjectBuildConfig buildConfig,
List<DotNetProjectInfo> changedProjects,
List<string> buildSucceededProjects
);
}
}

9
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<DotNetProjectInfo> Find(DotNetProjectBuildConfig buildConfig);
}
}

7
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.Cli.Build
{
public interface IDotNetProjectBuildConfigReader
{
DotNetProjectBuildConfig Read(string directoryPath);
}
}

9
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<string> Build(List<DotNetProjectInfo> projects, string arguments);
}
}

9
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<DotNetProjectInfo> projects);
}
}

9
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.Cli.Build
{
public interface IGitRepositoryHelper
{
string GetLastCommitId(GitRepository repository);
string GetFriendlyName(GitRepository repository);
}
}

9
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.Cli.Build
{
public interface IRepositoryBuildStatusStore
{
GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository);
void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status);
}
}

3
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs

@ -9,7 +9,8 @@ namespace Volo.Abp.Cli
public static string Log => Path.Combine(AbpRootPath, "cli", "logs");
public static string Root => Path.Combine(AbpRootPath, "cli");
public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin");
public static string Build => Path.Combine(AbpRootPath, "build");
private static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp");
}
}

132
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs

@ -0,0 +1,132 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
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 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 IBuildStatusGenerator BuildStatusGenerator { get; set; }
public IBuildProjectListSorter BuildProjectListSorter { get; set; }
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
var sw = new Stopwatch();
sw.Start();
var workingDirectory = commandLineArgs.Options.GetOrNull(
Options.WorkingDirectory.Short,
Options.WorkingDirectory.Long
);
var dotnetBuildArguments = commandLineArgs.Options.GetOrNull(
Options.DotnetBuildArguments.Short,
Options.DotnetBuildArguments.Long
);
var buildName = commandLineArgs.Options.GetOrNull(
Options.BuildName.Short,
Options.BuildName.Long
);
var forceBuild = commandLineArgs.Options.ContainsKey(Options.ForceBuild.Short) ||
commandLineArgs.Options.ContainsKey(Options.ForceBuild.Long);
var buildConfig = DotNetProjectBuildConfigReader.Read(workingDirectory ?? Directory.GetCurrentDirectory());
buildConfig.BuildName = buildName;
buildConfig.ForceBuild = forceBuild;
Console.WriteLine("Finding changed projects...");
var changedProjectFiles = ChangedProjectFinder.Find(buildConfig);
var buildSucceededProjects = DotNetProjectBuilder.Build(
changedProjectFiles,
dotnetBuildArguments ?? ""
);
var buildStatus = BuildStatusGenerator.Generate(
buildConfig,
changedProjectFiles,
buildSucceededProjects
);
RepositoryBuildStatusStore.Set(buildName, buildConfig.GitRepository, buildStatus);
sw.Stop();
Console.WriteLine("Build operation is completed in " + sw.ElapsedMilliseconds + " (ms)");
return Task.CompletedTask;
}
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("");
sb.AppendLine("-wd|--working-directory <directory-path> (default: empty)");
sb.AppendLine("-m |--max-parallel-builds <parallel-build-count> (default: 1)");
sb.AppendLine("-a |--dotnet-build-arguments <arguments> (default: empty)");
sb.AppendLine("-n |--build-name <name> (default: empty)");
sb.AppendLine("-f | --force (default: false)");
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 WorkingDirectory
{
public const string Short = "wd";
public const string Long = "working-directory";
}
public static class DotnetBuildArguments
{
public const string Short = "a";
public const string Long = "dotnet-build-arguments";
}
public static class BuildName
{
public const string Short = "n";
public const string Long = "build-name";
}
public static class ForceBuild
{
public const string Short = "f";
public const string Long = "force";
}
}
}
}

8
framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs

@ -188,18 +188,22 @@ namespace System.Collections.Generic
/// <typeparam name="T">The type of the members of values.</typeparam>
/// <param name="source">A list of objects to sort</param>
/// <param name="getDependencies">Function to resolve the dependencies</param>
/// <param name="comparer">Equality comparer for dependencies </param>
/// <returns>
/// Returns a new list ordered by dependencies.
/// If A depends on B, then B will come before than A in the resulting list.
/// </returns>
public static List<T> SortByDependencies<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> getDependencies)
public static List<T> SortByDependencies<T>(
this IEnumerable<T> source,
Func<T, IEnumerable<T>> getDependencies,
IEqualityComparer<T> comparer = null)
{
/* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp
* http://en.wikipedia.org/wiki/Topological_sorting
*/
var sorted = new List<T>();
var visited = new Dictionary<T, bool>();
var visited = new Dictionary<T, bool>(comparer);
foreach (var item in source)
{

79
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildProjectListSorter_Tests.cs

@ -0,0 +1,79 @@
using System.Collections.Generic;
using Shouldly;
using Xunit;
namespace Volo.Abp.Cli.Build
{
public class BuildProjectListSorter_Tests : AbpCliTestBase
{
private IBuildProjectListSorter _buildProjectListSorter;
public BuildProjectListSorter_Tests()
{
_buildProjectListSorter = GetRequiredService<IBuildProjectListSorter>();
}
[Fact]
public void SortByDependencies_Test()
{
// A -> B, C
// B -> D
// D -> F
// F -> C
// C -> G
// Final build order must be: G,
var repositoryName = "volo";
var source = new List<DotNetProjectInfo>
{
new DotNetProjectInfo(repositoryName, "A", true)
{
Dependencies = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo(repositoryName, "B", true),
new DotNetProjectInfo(repositoryName, "C", true)
}
},
new DotNetProjectInfo(repositoryName, "B", true)
{
Dependencies = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo(repositoryName, "D", true)
}
},
new DotNetProjectInfo(repositoryName, "D", true)
{
Dependencies = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo(repositoryName, "F", true)
}
},
new DotNetProjectInfo(repositoryName, "F", true)
{
Dependencies = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo(repositoryName, "C", true)
}
},
new DotNetProjectInfo(repositoryName, "C", true)
{
Dependencies = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo(repositoryName, "G", true)
}
},
new DotNetProjectInfo(repositoryName, "G", true)
};
var sortedDependencies =
_buildProjectListSorter.SortByDependencies(source, new DotNetProjectInfoEqualityComparer());
sortedDependencies.Count.ShouldBe(6);
sortedDependencies[0].CsProjPath.ShouldBe("G");
sortedDependencies[1].CsProjPath.ShouldBe("C");
sortedDependencies[2].CsProjPath.ShouldBe("F");
sortedDependencies[3].CsProjPath.ShouldBe("D");
sortedDependencies[4].CsProjPath.ShouldBe("B");
sortedDependencies[5].CsProjPath.ShouldBe("A");
}
}
}

123
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildStatusGenerator_Tests.cs

@ -0,0 +1,123 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Xunit;
namespace Volo.Abp.Cli.Build
{
public class BuildStatusGenerator_Tests : AbpCliTestBase
{
private readonly IBuildStatusGenerator _buildStatusGenerator;
private IGitRepositoryHelper _gitRepositoryHelper;
public BuildStatusGenerator_Tests()
{
_buildStatusGenerator = GetRequiredService<IBuildStatusGenerator>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
_gitRepositoryHelper = Substitute.For<IGitRepositoryHelper>();
services.AddTransient(provider => _gitRepositoryHelper);
}
[Fact]
public void Should_Set_Repository_CommitId_When_All_Projects_Built()
{
var buildConfig = new DotNetProjectBuildConfig
{
GitRepository = new GitRepository("volo", "dev", "")
{
DependingRepositories = new List<GitRepository>()
{
new GitRepository("abp", "dev", "")
}
}
};
var changedProjects = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo("volo", "project1.csproj", true)
};
var builtProjects = new List<string>()
{
"project1.csproj"
};
var lastCommitId = "1";
_gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
_gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("volo");
var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
status.CommitId.ShouldBe(lastCommitId);
}
[Fact]
public void Should_Set_Repository_CommitId_When_All_Projects_Built_For_Child_Repository()
{
var buildConfig = new DotNetProjectBuildConfig
{
GitRepository = new GitRepository("volo", "dev", "")
{
DependingRepositories = new List<GitRepository>()
{
new GitRepository("abp", "dev", "")
}
}
};
var changedProjects = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo("abp", "project1.csproj", true)
};
var builtProjects = new List<string>()
{
"project1.csproj"
};
var lastCommitId = "1";
_gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
_gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("abp");
var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
status.CommitId.ShouldBe(lastCommitId);
}
[Fact]
public void Should_Not_Set_Repository_CommitId_When_Building_Single_Solution()
{
var buildConfig = new DotNetProjectBuildConfig
{
GitRepository = new GitRepository("volo", "dev", "")
{
DependingRepositories = new List<GitRepository>()
{
new GitRepository("abp", "dev", "")
}
},
SlFilePath = "test.sln"
};
var changedProjects = new List<DotNetProjectInfo>()
{
new DotNetProjectInfo("volo", "project1.csproj", true)
};
var builtProjects = new List<string>()
{
"project1.csproj"
};
var lastCommitId = "1";
_gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
_gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("volo");
var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
status.CommitId.ShouldBeNull();
}
}
}

318
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepositoryBuildStatus_Tests.cs

@ -0,0 +1,318 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Xunit;
namespace Volo.Abp.Cli.Build
{
public class GitRepositoryBuildStatus_Tests : AbpCliTestBase
{
private IGitRepositoryHelper _gitRepositoryHelper;
protected override void AfterAddApplication(IServiceCollection services)
{
_gitRepositoryHelper = Substitute.For<IGitRepositoryHelper>();
services.AddTransient(provider => _gitRepositoryHelper);
}
[Fact]
public void Add_New_Build_Status_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project1.csproj",
CommitId = "1"
}
}
};
var newBuildStatus = new GitRepositoryBuildStatus(
existingBuildStatus.RepositoryName,
existingBuildStatus.BranchName
)
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project2.csproj",
CommitId = "2"
}
}
};
existingBuildStatus.MergeWith(newBuildStatus);
existingBuildStatus.SucceedProjects.Count.ShouldBe(2);
}
[Fact]
public void Update_Existing_Build_Status_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project1.csproj",
CommitId = "1"
}
}
};
var newBuildStatus = new GitRepositoryBuildStatus(
existingBuildStatus.RepositoryName,
existingBuildStatus.BranchName
)
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project1.csproj",
CommitId = "2"
},
new DotNetProjectBuildStatus
{
CsProjPath = "project2.csproj",
CommitId = "2"
}
}
};
existingBuildStatus.MergeWith(newBuildStatus);
existingBuildStatus.SucceedProjects.Count.ShouldBe(2);
existingBuildStatus.GetSelfOrChild("volo").SucceedProjects.First(p => p.CsProjPath == "project1.csproj")
.CommitId.ShouldBe("2");
existingBuildStatus.GetSelfOrChild("volo").SucceedProjects.First(p => p.CsProjPath == "project2.csproj")
.CommitId.ShouldBe("2");
}
[Fact]
public void Add_New_Build_Status_For_Child_Repository_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("abp", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project1.csproj",
CommitId = "1"
}
}
}
}
};
var newBuildStatus = new GitRepositoryBuildStatus(
existingBuildStatus.RepositoryName,
existingBuildStatus.BranchName
)
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("abp", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "project2.csproj",
CommitId = "2"
}
}
}
}
};
existingBuildStatus.MergeWith(newBuildStatus);
existingBuildStatus.GetChild("abp").SucceedProjects.Count.ShouldBe(2);
}
[Fact]
public void Should_Update_Repository_CommitId_When_New_CommitId_Is_Not_Empty()
{
var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev");
var newBuildStatus = new GitRepositoryBuildStatus(
existingBuildStatus.RepositoryName,
existingBuildStatus.BranchName
)
{
CommitId = "42"
};
existingBuildStatus.MergeWith(newBuildStatus);
existingBuildStatus.CommitId.ShouldBe("42");
}
[Fact]
public void Should_Not_Update_Repository_CommitId_When_New_CommitId_Is_Empty()
{
var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
{
CommitId = "21"
};
var newBuildStatus = new GitRepositoryBuildStatus(
existingBuildStatus.RepositoryName,
existingBuildStatus.BranchName
)
{
CommitId = ""
};
existingBuildStatus.MergeWith(newBuildStatus);
existingBuildStatus.CommitId.ShouldBe("21");
}
[Fact]
public void GetChild_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-2", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-3", "dev")
}
},
new GitRepositoryBuildStatus("repo-4", "dev")
}
};
existingBuildStatus.GetChild("repo-3").RepositoryName.ShouldBe("repo-3");
existingBuildStatus.GetChild("repo-4").RepositoryName.ShouldBe("repo-4");
}
[Fact]
public void GetUniqueName_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-2", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-3", "dev")
}
},
new GitRepositoryBuildStatus("repo-4", "dev")
}
};
existingBuildStatus.GetUniqueName("").ShouldBe("B25C935F97D7B3375530A96B392B7644");
existingBuildStatus.GetUniqueName("production").ShouldBe("production_B25C935F97D7B3375530A96B392B7644");
}
[Fact]
public void GetSelfOrChild_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-2", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-3", "dev")
}
},
new GitRepositoryBuildStatus("repo-4", "dev")
}
};
existingBuildStatus.GetSelfOrChild("repo-1").RepositoryName.ShouldBe("repo-1");
existingBuildStatus.GetSelfOrChild("repo-2").RepositoryName.ShouldBe("repo-2");
existingBuildStatus.GetSelfOrChild("repo-3").RepositoryName.ShouldBe("repo-3");
existingBuildStatus.GetSelfOrChild("repo-4").RepositoryName.ShouldBe("repo-4");
}
[Fact]
public void AddOrUpdateProjectStatus_Test()
{
var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
{
DependingRepositories = new List<GitRepositoryBuildStatus>()
{
new GitRepositoryBuildStatus("repo-2", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "A.csproj",
CommitId = "42"
}
},
DependingRepositories = new List<GitRepositoryBuildStatus>
{
new GitRepositoryBuildStatus("repo-3", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>
{
new DotNetProjectBuildStatus
{
CsProjPath = "B.csproj",
CommitId = "42"
}
}
}
}
},
new GitRepositoryBuildStatus("repo-4", "dev")
{
SucceedProjects = new List<DotNetProjectBuildStatus>()
{
new DotNetProjectBuildStatus
{
CsProjPath = "C.csproj",
CommitId = "42"
}
}
}
}
};
var repo2 = existingBuildStatus.GetChild("repo-2");
repo2.AddOrUpdateProjectStatus(new DotNetProjectBuildStatus
{
CommitId = "21",
CsProjPath = "A.csproj"
});
var repo3 = existingBuildStatus.GetChild("repo-3");
repo3.AddOrUpdateProjectStatus(new DotNetProjectBuildStatus
{
CommitId = "21",
CsProjPath = "X.csproj"
});
repo2.SucceedProjects.Count.ShouldBe(1);
repo2.SucceedProjects.ShouldContain(e=> e.CsProjPath == "A.csproj" && e.CommitId == "21");
repo3.SucceedProjects.Count.ShouldBe(2);
repo3.SucceedProjects.ShouldContain(e=> e.CsProjPath == "X.csproj" && e.CommitId == "21");
}
}
}

52
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepository_Tests.cs

@ -0,0 +1,52 @@
using System.Collections.Generic;
using Shouldly;
using Xunit;
namespace Volo.Abp.Cli.Build
{
public class GitRepository_Tests : AbpCliTestBase
{
[Fact]
public void GetUniqueName_Test()
{
var gitRepository = new GitRepository("repo-1", "dev", "")
{
DependingRepositories = new List<GitRepository>
{
new GitRepository("repo-2", "dev", ""),
new GitRepository("repo-3", "dev", "")
{
DependingRepositories = new List<GitRepository>()
{
new GitRepository("repo-4", "dev", "")
}
}
}
};
gitRepository.GetUniqueName("").ShouldBe("B25C935F97D7B3375530A96B392B7644");
gitRepository.GetUniqueName("production").ShouldBe("production_B25C935F97D7B3375530A96B392B7644");
}
[Fact]
public void FindRepositoryOf_Test()
{
var gitRepository = new GitRepository("repo-1", "dev", "/repo-1/dev/")
{
DependingRepositories = new List<GitRepository>
{
new GitRepository("repo-2", "dev", "/repo-2/dev/"),
new GitRepository("repo-3", "dev", "/repo-3/dev/")
{
DependingRepositories = new List<GitRepository>()
{
new GitRepository("repo-4", "dev", "/repo-4/dev/")
}
}
}
};
gitRepository.FindRepositoryOf("/repo-4/dev/A.csproj").ShouldBe("repo-4");
}
}
}
Loading…
Cancel
Save