var VERSION='0.2.1' use-ci-loggers use-release-management use namespace='System' use namespace='System.IO' use namespace='System.Collections.Concurrent' use namespace='System.Collections.Generic' use namespace='System.Net' use namespace='System.Linq' use namespace='System.Text' use namespace='System.Text.RegularExpressions' use namespace='System.Threading.Tasks' use import="BuildEnv" use import="EnvironmentParameters" use import="Files" use import="Json" functions @{ const string DefaultBuildBranch = "dev"; const string DefaultCoherenceCacheDir = ".coherence-cache"; const string UniverseCommitsFileName = "commits-universe"; const string CoherenceCacheVersionFileName = "coherence-version"; private static bool Quiet { get; set; } static string baseDir = Directory.GetCurrentDirectory(); static string targetDir = Path.Combine(baseDir, "artifacts", "build"); static string buildBranch = GetEnvironmentParameter("BUILD_BRANCH") ?? DefaultBuildBranch; static string coherencePath = GetEnvironmentParameter("COHERENCE_PATH"); static string dropsShare = GetEnvironmentParameter("ASPNETCI_DROPS_SHARE", DefaultDropsShare); static string kBuildVersion = GetEnvironmentParameter("DNX_BUILD_VERSION"); static string ciVolatileShare = GetEnvironmentParameter("CI_VOLATILE_SHARE"); static string koreBuildTargets = GetEnvironmentParameter("KOREBUILD_BUILD_TARGETS"); static string nugetVersion = GetEnvironmentParameter("NUGET_VERSION"); static string nugetExe = GetEnvironmentParameter("PUSH_NUGET_EXE"); static string universeCommitsFile = GetEnvironmentParameter("UNIVERSE_COMMITS_FILE"); static bool skipNoCredentials = GetEnvironmentParameter("UNIVERSE_SKIP_NO_CREDENTIALS", value => value == "1"); static string repositoryInclude = GetEnvironmentParameter("KOREBUILD_REPOSITORY_INCLUDE"); static string repositoryExclude = GetEnvironmentParameter("KOREBUILD_REPOSITORY_EXCLUDE"); static string gateBranch = GetEnvironmentParameter("KOREBUILD_GATE_BRANCH"); private static bool UseGateBranch = !string.IsNullOrEmpty(gateBranch); // Doesn't build on Mono since their contracts don't match string[] repositories = GetRepositoriesToBuild().ToArray(); string[] noSrcRepositoryExclude = GetNoSrcDeleteRepositories().ToArray(); static bool useHttps = UseHttps(baseDir); static string gitHubUriPrefix = useHttps ? "https://github.com/aspnet/" : "git@github.com:aspnet/"; } var buildTarget = "compile" @{ if (!string.IsNullOrEmpty(kBuildVersion)) { VERSION += "-" + kBuildVersion; } else { VERSION += "-" + BuildNumber; } } #default .compile #--quiet @{ Quiet = true; } #pull #compile .pull #install .pull #pack directory create='${targetDir}' #pack-install .pack nuget-local-publish sourcePackagesDir='${targetDir}' #git-pull target='pull' @{ Parallel.ForEach(repositories, repo => { CloneOrUpdate(repo); }); } #update @{ Parallel.ForEach(repositories, repo => { Git("pull --ff-only", repo); }); } #sync-commits @{ if (string.IsNullOrEmpty(universeCommitsFile)) { throw new Exception("UNIVERSE_COMMITS_FILE not specified."); } var commitsToSync = GetCommitsToSync(universeCommitsFile); Parallel.ForEach(repositories, repo => { if (commitsToSync.ContainsKey(repo) && Directory.Exists(repo)) { var syncHash = commitsToSync[repo]; Console.WriteLine(string.Format("Syncing {0} to {1}", repo, syncHash)); Git("reset --hard " + syncHash, repo); } }); } #verify-all .pull .change-default-build-target-to-verify .build-all #ci-test .pull .use-local-coherence .sync-commits .remove-src-folders .change-default-build-target-to-verify .build-all #ci-pull @{ var threads = int.Parse(Environment.GetEnvironmentVariable("UNIVERSE_THREADS") ?? "4"); var blockLogger = Log as IBlockLogger; if (blockLogger != null) { blockLogger.StartBlock("Cloning repos"); } Parallel.ForEach(repositories, new ParallelOptions { MaxDegreeOfParallelism = threads }, repo => { var useBuildBranch = true; if (Directory.Exists(repo)) { Directory.Delete(repo, recursive: true); } if (UseGateBranch) { try { GitCommand("", string.Format("clone --quiet --depth 1 --branch {0} git@github.com:aspnet/{1}.git", gateBranch, repo)); useBuildBranch = false; Log.Warn(string.Format( "{0} has a branch '{1}' which is overriding use of {2}. {1} must be deleted before {2} will be used.", repo, gateBranch, buildBranch)); } catch (Exception ex) { Log.Info(string.Format("{0} doesn't exist, falling back to {1}.", gateBranch, buildBranch)); } } if (useBuildBranch) { GitCommand("", string.Format("clone --quiet --depth 1 --branch {0} git@github.com:aspnet/{1}.git", buildBranch, repo)); } }); if (blockLogger != null) { blockLogger.EndBlock("Cloning repos"); } } #show-build-graph @{ var batchedRepos = GetBuildGraph(); Log.Info("Building repositories in batches: "); foreach (var repos in batchedRepos) { Log.Info(string.Format("{0} - {1}", repos.Key, string.Join(", ", repos))); } } #ci-build @{ var nugetExe = Path.Combine(Directory.GetCurrentDirectory(), ".build", "nuget.exe"); var universeArtifacts = Path.Combine(Directory.GetCurrentDirectory(), "artifacts"); var universeBuild = Path.Combine(universeArtifacts, "build"); var packagesPublishDir = Path.Combine(Directory.GetCurrentDirectory(), ".nuget", "publishDir"); // Historically we've always produced coherent builds using this target. Unless specified otherwise, // we'll continue to produce coherent builds here. var createCoherentBuild = Environment.GetEnvironmentVariable("CREATE_COHERENT_BUILD") != "false"; // Snapshot the .build folder if (!IsLinux) { Exec("cmd", "/C xcopy /S/Q/I/Y .build " + Path.Combine(universeArtifacts, ".build"), ""); } else { CopyFolder(".build", Path.Combine(universeArtifacts, ".build"), true); } var blockLogger = Log as IBlockLogger; var commits = new ConcurrentDictionary(); var threads = int.Parse(Environment.GetEnvironmentVariable("UNIVERSE_THREADS") ?? "4"); buildTarget = koreBuildTargets ?? "--quiet compile nuget-verify nuget-install"; var batchedRepos = GetBuildGraph(); Log.Info("Building repositories in batches: "); foreach (var repos in batchedRepos) { Log.Info(string.Format("{0} - {1}", repos.Key, string.Join(", ", repos))); } Directory.CreateDirectory(universeBuild); if (createCoherentBuild) { if (!IsLinux) { // Publish to a directory and use that as a package source for later builds. This doesn't work in Linux due to // https://github.com/NuGet/Home/issues/2383 Directory.CreateDirectory(packagesPublishDir); Environment.SetEnvironmentVariable("NUGET_VOLATILE_FEED_ARTIFACTS", packagesPublishDir); Environment.SetEnvironmentVariable("PACKAGES_PUBLISH_DIR", packagesPublishDir); } else { Environment.SetEnvironmentVariable("NUGET_VOLATILE_FEED_ARTIFACTS", universeBuild); } } if (Environment.GetEnvironmentVariable("UNIVERSE_PUSH_TO_VOLATILE") == "true") { Environment.SetEnvironmentVariable("NUGET_PUBLISH_FEED", "https://dotnet.myget.org/F/aspnetcore-volatile-dev/api/v2/package"); } var universeToolsVersion = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "ToolsVersion.txt")).Trim(); foreach (var batch in batchedRepos) { Parallel.ForEach(batch.ToArray(), new ParallelOptions { MaxDegreeOfParallelism = threads }, repo => { var blockName = string.Format("Building {0}", repo); if (blockLogger != null) { blockLogger.StartBlock(blockName); } // begin workaround // as we transition to VS 2017, some repos will build with project.json and others with csproj // the 'csproj' version of KoreBuild is the 'feature/msbuild' branch of that repo. project.json // support is on 'dev'. This workaround avoids installing the version of KoreBuild used by // individual repos unless it is already upgraded to the version currently used by Universe. var repoToolsVersion = universeToolsVersion; var buildPs1 = Path.Combine(repo, "build.ps1"); if (File.Exists(buildPs1)) { foreach (var line in File.ReadAllLines(buildPs1)) { var match = Regex.Match(line, @".*https://github.com/aspnet/KoreBuild/archive/(.+)\.zip.*"); if (match != null && match.Success) { repoToolsVersion = match.Groups[1].Value; break; } } } Log.Info(repo + " KoreBuild version = " + repoToolsVersion + ", Universe = " + universeToolsVersion); // end workaround if (repoToolsVersion == universeToolsVersion) { if (!IsLinux) { Exec("cmd", "/C xcopy /S/Q/I/Y .build " + Path.Combine(repo, ".build"), ""); } else { CopyFolder(".build", Path.Combine(repo, ".build"), true); } } try { Exec(CreateBuildWithFlowId(repo), buildTarget, repo); var repoArtifacts = Path.Combine(repo, "artifacts"); var repoBuild = Path.Combine(repoArtifacts, "build"); if (Directory.Exists(repoBuild)) { foreach (var source in Directory.EnumerateFiles(repoBuild, "*.nupkg")) { File.Copy(source, Path.Combine(universeBuild, Path.GetFileName(source)), overwrite: true); } if (Environment.GetEnvironmentVariable("KOREBUILD_ADD_ASSEMBLY_INFO") == "1") { var commitFile = Path.Combine(repoArtifacts, "commit"); if (File.Exists(commitFile)) { commits.TryAdd(repo, File.ReadAllLines(commitFile)[0]); } } if (!string.IsNullOrEmpty(ciVolatileShare)) { Log.Info("Publishing packages to " + ciVolatileShare); NuGetPackagesAdd(repoBuild, ciVolatileShare); } } Log.Info(string.Format("Build {0} succeeded", repo)); } catch (Exception ex) { Log.Error("Building '" + repo + "' failed: " + ex); throw; } finally { if (blockLogger != null) { blockLogger.EndBlock(blockName); } } }); } var commitsAsString = string.Join("\n", commits.Select(c => c.Key + ":" + c.Value)); File.WriteAllText(Path.Combine(universeArtifacts, "commits"), commitsAsString); } #remove-src-folders @{ foreach (var repo in GetRepositoriesToBuild()) { if (!noSrcRepositoryExclude.Contains(repo)) { RemoveSrcFolder(repo); } else { Console.WriteLine("Keeping the sources for " + repo + " because it's explicitly excluded"); } } } #use-local-coherence description='Sets up environment variables to use Coherence at COHERENCE_PATH' @{ if (string.IsNullOrEmpty(coherencePath)) { throw new Exception("COHERENCE_PATH not specified."); } if (string.IsNullOrEmpty(universeCommitsFile)) { universeCommitsFile = Path.Combine(coherencePath, UniverseCommitsFileName); if (!File.Exists(universeCommitsFile)) { throw new Exception("Universe commits file could not be found at " + universeCommitsFile); } } var coherencePackages = Path.Combine(coherencePath, "packages-expanded"); if (!Directory.Exists(coherencePackages)) { coherencePackages = Path.Combine(coherencePath, "build"); } if (!Directory.Exists(coherencePackages)) { throw new Exception("Packages directory could not be located under " + coherencePath); } Environment.SetEnvironmentVariable("NUGET_VOLATILE_FEED_AspNetCore", coherencePackages); } #change-default-build-target-to-verify - buildTarget = "verify"; #change-default-build-target-for-coherence-build - buildTarget = koreBuildTargets ?? "compile nuget-install"; #init @{ var templatePath = Path.Combine(baseDir, "build-template"); var templateFiles = Files.Include(templatePath + Path.DirectorySeparatorChar + "*.*").Select(Path.GetFileName).ToList(); foreach(var repo in repositories) { foreach (string fileName in templateFiles) { var targetFile = Path.Combine(Directory.GetCurrentDirectory(), repo, fileName); var sourceFile = Path.Combine(Directory.GetCurrentDirectory(), templatePath, fileName); // Don't update the makefile if (fileName.Equals("makefile.shade", StringComparison.OrdinalIgnoreCase) && File.Exists(targetFile)) { continue; } // Don't update extra configuration files if (fileName.Equals("NuGet.master.config", StringComparison.OrdinalIgnoreCase) || fileName.Equals("NuGet.release.config", StringComparison.OrdinalIgnoreCase)) { continue; } if (!File.Exists(targetFile) || (File.ReadAllText(sourceFile) != File.ReadAllText(targetFile))) { Log.Info("Updating " + fileName + " to " + repo); File.Copy(sourceFile, targetFile, true); } } } } #pull-all -Parallel.ForEach(GetAllRepos(), CloneOrUpdate); #only-compile .build-all -Log.Warn("only-compile target is deprecated. Use build-all"); #build-all target='compile' @{ var failed = new Dictionary(); var skipped = new List(); var universeArtifacts = Path.Combine("artifacts", "build"); Directory.CreateDirectory(universeArtifacts); foreach(var repo in repositories) { var blockName = string.Format("Building {0}", repo); if (IsTeamCity) { Log.Info(string.Format("##teamcity[blockOpened name='{0}']", FormatForTeamCity(blockName))); } try { Log.Info(blockName); if (skipNoCredentials) { if (!Directory.Exists(repo)) { // The directory was not cloned because the credentials were not specified so don't try to build it skipped.Add(repo); Log.Warn(string.Format("The repo {0} does not exist locally and it will not be built.", repo)); continue; } } if (IsMono) { Exec(Path.Combine(repo, "build.sh"), buildTarget, repo); } else { Exec("build.cmd", buildTarget, repo); } var repoArtifacts = Path.Combine(repo, "artifacts", "build"); if (Directory.Exists(repoArtifacts)) { foreach (var source in Directory.EnumerateFiles(repoArtifacts, "*.nupkg")) { File.Copy(source, Path.Combine(universeArtifacts, Path.GetFileName(source)), overwrite: true); } if (!string.IsNullOrEmpty(ciVolatileShare)) { Log.Info("Publishing packages to " + ciVolatileShare); if (string.IsNullOrEmpty(nugetExe)) { Log.Warn("PUSH_NUGET_EXE not specified."); } else { NuGetPackagesAdd(repoArtifacts, ciVolatileShare); } } } Log.Info(string.Format("Build {0} succeeded", repo)); } catch (Exception ex) { Log.Warn(string.Format("Build {0} failed: {1}", repo, ex.Message)); failed[repo] = ex; } finally { if (IsTeamCity) { Log.Info(string.Format("##teamcity[blockClosed name='{0}']", FormatForTeamCity(blockName))); } } } foreach(var repo in repositories) { Exception ex; if (failed.TryGetValue(repo, out ex)) { Log.Warn(string.Format("Build {0} failed: {1}", repo, ex.Message)); if (IsTeamCity) { Log.Warn(string.Format("##teamcity[message text='{0}' errorDetails='{1}' status='ERROR']", FormatForTeamCity(ex.Message), FormatForTeamCity(ex.StackTrace))); } } else if (skipped.Contains(repo)) { Log.Warn(string.Format("Build {0} skipped", repo)); } else { Log.Info(string.Format("Build {0} succeeded", repo)); } } if (failed.Any()) { throw new Exception("One or more repos failed to build"); } } #only-install target='install' @{ foreach(var repo in repositories) { if (IsMono) { Exec(Path.Combine(repo, "build.sh"), "install", repo); } else { Exec("build.cmd", "install", repo); } } } #git-status description='Show status of repos known by Universe' @{ foreach(var repo in repositories) { GitStatus(repo); } } #git-clean description='REMOVE ALL CHANGES to the working directory' @{ Console.WriteLine("This runs `git clean -xfd` in all non-Universe repos."); Console.WriteLine("This should REMOVE ALL CHANGES to the working directory."); Console.Write("***** Are you sure? ***** (Y or anything else)? "); if (Console.ReadLine() != "Y") { throw new Exception("git-clean cancelled"); } foreach(var repo in repositories) { GitClean(repo); } } macro name='ExecClr' program='string' commandline='string' exec-clr macro name='Git' gitCommand='string' gitFolder='string' git macro name='GitPull' gitUri='string' gitBranch='string' gitFolder='string' git-pull macro name='GitClone' gitUri='string' gitBranch='string' git-clone macro name='GitConfig' gitOptionName='string' gitOptionValue='string' gitFolder='string' git-config macro name='GitStatus' gitFolder='string' git gitCommand='status' macro name='GitClean' gitFolder='string' git gitCommand='clean -xdf' macro name='GitCommand' gitFolder='string' gitCommand='string' git macro name='Exec' program='string' commandline='string' workingdir='string' exec macro name='NuGetPackagesAdd' sourcePackagesDir='string' targetPackagesDir='string' nuget-packages-add macro name="CopyFolder" sourceDir='string' outputDir='string' overwrite='bool' copy functions @{ static IDictionary GetCommitsToSync(string commitsFile) { var commits = new Dictionary(); if (string.IsNullOrEmpty(commitsFile)) { return commits; } Console.WriteLine("Using commits file: " + commitsFile); var lines = File.ReadAllLines(commitsFile); foreach(var line in lines) { var parts = line.Split(new string[] {":"}, StringSplitOptions.RemoveEmptyEntries); commits.Add(parts[0], parts[1]); } return commits; } static bool UseHttps(string directory) { var filename = Path.Combine(directory, ".git", "config"); if (!File.Exists(filename)) { Console.WriteLine("Unable to find '{0}' file", filename); return false; } var url = ReadOriginUrl(filename); return IsHttpsUrl(url); } // Perform equivalent of `git config remote.origin.url` but directly // read config file to get value. static string ReadOriginUrl(string filename) { // Subsection portion of configuration name is case-sensitive; rest // of name is case-insensitive. var beginOriginSection = new Regex(@"^\[(?i:remote) ""origin""\]\s*$"); var beginSection = new Regex(@"^\["); var urlLine = new Regex(@"^\s+url = (\S+)\s*$", RegexOptions.IgnoreCase); var inRemoteOriginSection = false; foreach (var line in File.ReadAllLines(filename)) { if (beginOriginSection.IsMatch(line)) { inRemoteOriginSection = true; continue; } if (inRemoteOriginSection) { if (beginSection.IsMatch(line)) { // Read through the section without finding URL line. break; } var match = urlLine.Match(line); if (match.Success && match.Groups.Count == 2 && match.Groups[1].Success) { return match.Groups[1].Value; } } } Console.WriteLine("Unable to parse '{0}' file", filename); return null; } static bool IsHttpsUrl(string url) { if (string.IsNullOrEmpty(url)) { return false; } return url.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase); } static bool IsAccessible(string key) { var req = WebRequest.CreateHttp("https://github.com/aspnet/" + key); req.Method = "HEAD"; try { using (req.GetResponse()) { // intentionally empty } } catch (WebException ex) { if (ex.Response != null && ((HttpWebResponse)ex.Response).StatusCode == HttpStatusCode.NotFound) { return false; } // Ignore any other exception. They should surface as part of git clone with well-known messages. } return true; } void RemoveSrcFolder(string repo) { var srcDir = Path.Combine(repo, "src"); if (Directory.Exists(srcDir)) { Console.WriteLine("Deleting " + srcDir); Directory.Delete(srcDir, recursive: true); } } void CloneOrUpdate(string repo) { var repoUrl = gitHubUriPrefix + repo + ".git"; if (Directory.Exists(repo)) { GitPull(repoUrl, buildBranch, repo); } else { if (useHttps && !IsAccessible(repo)) { if (skipNoCredentials) { // This is used on the XPlat CI. Must return because otherwise git will wait for user // input and hang the build Log.Warn(string.Format("The repo at '{0}' is not accesible and it will not be cloned.", repoUrl)); return; } Log.Warn(string.Format("The repo at '{0}' is not accessible. If you do not have access to this repository, skip the git prompt" + " for credentials to skip cloning this repository. To avoid this prompt, re-clone the Universe repository over ssh.", repoUrl)); } try { GitClone(repoUrl, buildBranch); } catch (Exception ex) { Log.Warn(string.Format("Unable to clone repository at '{0}': {1}", repoUrl, ex.Message)); return; } } } string FormatForTeamCity(string input) { return input.Replace("|", "||") .Replace("'", "|'") .Replace("\r", "|r") .Replace("\n", "|n") .Replace("]", "|]"); } static IEnumerable GetRepositoriesToBuild() { IEnumerable reposToBuild = new HashSet(StringComparer.OrdinalIgnoreCase) { "PlatformAbstractions", "Common", "JsonPatch", "FileSystem", "Configuration", "DependencyInjection", "EventNotification", "Options", "Logging", "DotNetTools", "HtmlAbstractions", "DataProtection", "HttpAbstractions", "Testing", "Microsoft.Data.Sqlite", "Caching", "Razor", "RazorTooling", "Hosting", "AzureIntegration", "EntityFramework", "EntityFramework.Tools", "WebListener", "KestrelHttpServer", "IISIntegration", "ServerTests", "ResponseCaching", "Session", "CORS", "Routing", "StaticFiles", "Diagnostics", "Security", "Antiforgery", "WebSockets", "Localization", "BasicMiddleware", "Proxy", "Mvc", "MvcPrecompilation", "Identity", "Scaffolding", "SignalR", "BrowserLink", "Entropy", "MusicStore", "MetaPackages", }; if (!string.IsNullOrEmpty(repositoryInclude)) { reposToBuild = new HashSet( repositoryInclude.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase); } if (!string.IsNullOrEmpty(repositoryExclude)) { var reposToExclude = repositoryExclude.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries); reposToBuild = reposToBuild.Except(reposToExclude); } return reposToBuild.ToList(); } static IEnumerable GetNoSrcDeleteRepositories() { var repositoryExclude = Environment.GetEnvironmentVariable("KOREBUILD_NOSRC_EXCLUDE"); if (string.IsNullOrEmpty(repositoryExclude)) { return new string[0]; } return repositoryExclude.Split(new string[] {","}, StringSplitOptions.RemoveEmptyEntries); } static string DefaultDropsShare(string value) { return value ?? (Environment.OSVersion.Platform == PlatformID.Unix ? "/aspnetci-drops" : @"\\aspnetci\drops"); } string CreateBuildWithFlowId(string repo) { string output; if (IsLinux) { output = Path.Combine(repo, "build.sh"); } else { output = Path.Combine(repo, "build-with-flow-id.cmd"); File.WriteAllText( output, string.Format("set KOREBUILD_FLOWID=KOREBUILD_{0}& call build.cmd %*", repo)); } return Path.GetFullPath(output); } static IList> GetBuildGraph() { var repositoryLookup = GetRepositoriesToBuild() .Select(RepoInfoFactory.Create) .ToList(); foreach (var info in repositoryLookup) { foreach (var item in info.DependencyNames) { var dependency = repositoryLookup.Find(r => r.RepositoryNames.Contains(item)); if (dependency != null) { info.Dependencies.Add(dependency); } } } return repositoryLookup.GroupBy(r => r.Order, r => r.Name).OrderBy(r => r.Key).ToArray(); } private class RepoInfoFactory { public static RepositoryInfo Create(string repo) { var info = new RepositoryInfo { Name = repo }; ResolveProjectJsonProjects(info, repo); ResolveMsBuildProjects(info, repo); info.DependencyNames.ExceptWith(info.RepositoryNames); return info; } private static void ResolveMsBuildProjects(RepositoryInfo info, string repo) { foreach (var sln in Directory.EnumerateFiles(repo, "*.sln")) { var lines = File.ReadAllLines(sln); foreach (var line in lines) { var match = Regex.Match(line, @"^# Visual Studio (\d+)\s?"); if (match == null || match.Groups.Count < 2) { continue; } int version; if (int.TryParse(match.Groups[1].Value, out version) && version >= 15) { ResolveMsBuildProject(info, sln); break; } } } } private static void ResolveMsBuildProject(RepositoryInfo info, string projectFile) { var intermediate = Path.Combine(Directory.GetCurrentDirectory(), "obj"); Directory.CreateDirectory(intermediate); var dgJson = Path.Combine(intermediate, Path.GetFileName(projectFile) + ".graph.json"); var psi = new ProcessStartInfo { UseShellExecute = false, FileName = "dotnet", Arguments = "msbuild \"" + projectFile + "\" /t:GenerateRestoreGraphFile \"/p:RestoreGraphOutputPath=" + dgJson + "\"" }; var p = Process.Start(psi); p.WaitForExit(); if (p.ExitCode != 0) { Console.WriteLine("warn: Failed to get restore graph from " + projectFile); return; } JsonObject graph; try { graph = (JsonObject)Json.Deserialize(File.ReadAllText(dgJson)); } catch (Exception ex) { Console.Error.WriteLine("Failed to parse dependency graph"); throw; } var format = graph.ValueAsInt("format"); if (format != 1) { Console.Error.WriteLine("Dependency graph file in unsupported format version: " + format); throw new FormatException(); } var projects = graph.ValueAsJsonObject("projects"); foreach (var projectKey in projects.Keys) { var project = projects.ValueAsJsonObject(projectKey); var restore = project.ValueAsJsonObject("restore"); var projectName = restore.ValueAsString("projectName"); info.RepositoryNames.Add(projectName); var frameworks = project.ValueAsJsonObject("frameworks"); foreach (var fxKey in frameworks.Keys) { var fxDeps = frameworks.ValueAsJsonObject(fxKey).ValueAsJsonObject("dependencies"); foreach (var depKey in fxDeps.Keys) { var dep = fxDeps.ValueAsJsonObject(depKey); if (dep.ValueAsString("target") != "Package") { continue; } // todo version? info.DependencyNames.Add(depKey); } } } } private static void ResolveProjectJsonProjects(RepositoryInfo info, string repo) { var srcDir = Path.Combine(repo, "src"); if (Directory.Exists(srcDir)) { foreach (var directory in Directory.EnumerateDirectories(srcDir)) { info.RepositoryNames.Add(Path.GetFileName(directory)); var projectJson = Path.Combine(directory, "project.json"); if (File.Exists(projectJson)) { ResolveDependencies(projectJson, info.DependencyNames); } } } var otherDirs = new[] { "test", "samples", "tools" }; for (var i = 0; i < otherDirs.Length; i++) { var otherDir = Path.Combine(repo, otherDirs[i]); if (Directory.Exists(otherDir)) { foreach (var directory in Directory.EnumerateDirectories(otherDir)) { var projectJson = Path.Combine(directory, "project.json"); if (File.Exists(projectJson)) { ResolveDependencies(projectJson, info.DependencyNames); } else { // Look for test\Websites\WebsiteName\project.json foreach (var subDirectory in Directory.EnumerateDirectories(directory)) { projectJson = Path.Combine(subDirectory, "project.json"); if (File.Exists(projectJson)) { ResolveDependencies(projectJson, info.DependencyNames); } } } } } } } private static void ResolveDependencies(string projectJsonPath, HashSet dependencies) { JsonObject project; try { project = (JsonObject)Json.Deserialize(File.ReadAllText(projectJsonPath)); } catch (Exception ex) { Console.Error.WriteLine("Failed to parse project at path " + projectJsonPath + Environment.NewLine + ex.Message); throw; } var dependenciesNode = project.ValueAsJsonObject("dependencies"); AddKeys(dependencies, dependenciesNode); var frameworkNodes = project.ValueAsJsonObject("frameworks"); if (frameworkNodes != null) { foreach (var framework in frameworkNodes.Keys) { dependenciesNode = frameworkNodes.ValueAsJsonObject(framework).ValueAsJsonObject("dependencies"); AddKeys(dependencies, dependenciesNode); } } AddKeys(dependencies, project.ValueAsJsonObject("tools")); } private static void AddKeys(HashSet target, JsonObject source) { if (source != null) { foreach (var key in source.Keys) { target.Add(key); } } } } private class RepositoryInfo { public string Name; public HashSet RepositoryNames = new HashSet(StringComparer.OrdinalIgnoreCase); public HashSet DependencyNames = new HashSet(StringComparer.OrdinalIgnoreCase); public HashSet Dependencies = new HashSet(); public int Order { get { return GetOrder(new List(), this); } } private static int GetOrder(List visited, RepositoryInfo info) { if (visited.Contains(info)) { throw new Exception("A cyclic dependency between the following repositories has been detected: " + string.Join(" -> ", visited)); } visited.Add(info); var order = 0; foreach (var dependency in info.Dependencies) { order = Math.Max(order, GetOrder(visited, dependency)); } visited.RemoveAt(visited.Count - 1); return order + 1; } public override string ToString() { return Name; } } }