| File: src\UsageReport\WritePackageUsageData.cs | Web Access |
| Project: src\arcade\src\Microsoft.DotNet.SourceBuild\tasks\Microsoft.DotNet.SourceBuild.Tasks.csproj (Microsoft.DotNet.SourceBuild.Tasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NuGet.Packaging.Core; using NuGet.Versioning; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using Task = Microsoft.Build.Utilities.Task; namespace Microsoft.DotNet.SourceBuild.Tasks.UsageReport { public class WritePackageUsageData : Microsoft.Build.Utilities.Task { public string[] RestoredPackageFiles { get; set; } public string[] TarballPrebuiltPackageFiles { get; set; } public string[] ReferencePackageFiles { get; set; } public string[] SourceBuiltPackageFiles { get; set; } /// <summary> /// Specific PackageInfo items to check for usage. An alternative to passing lists of nupkgs /// when the nupkgs have already been parsed to get package info items. /// /// %(Identity): Path to the original nupkg. /// %(PackageId): Identity of the package. /// %(PackageVersion): Version of the package. /// </summary> public ITaskItem[] NuGetPackageInfos { get; set; } /// <summary> /// runtime.json files (from Microsoft.NETCore.Platforms) to use to look for the set of all /// possible runtimes. This is used to determine which part of a package id is its /// 'runtime.{rid}.' prefix, if it has the prefix. /// </summary> public string[] PlatformsRuntimeJsonFiles { get; set; } /// <summary> /// Keep track of the RID built that caused these usages. /// </summary> public string TargetRid { get; set; } /// <summary> /// Project directories to scan for project.assets.json files. If these directories contain /// one another, the project.assets.json files is reported as belonging to the first project /// directory that contains it. For useful results, put the leafmost directories first. /// /// This isn't used here, but it's included in the usage data so report generation can /// happen independently of commits that add/remove submodules. /// </summary> public string[] ProjectDirectories { get; set; } /// <summary> /// A root dir that contains all ProjectDirectories. This is used to find the relative path /// of each usage. /// </summary> [Required] public string RootDir { get; set; } /// <summary> /// project.assets.json files to ignore, for example, because they are checked-in assets not /// generated during source-build and cause false positives. /// </summary> public string[] IgnoredProjectAssetsJsonFiles { get; set; } /// <summary> /// Output usage data JSON file path. /// </summary> [Required] public string DataFile { get; set; } /// <summary> /// If passed, the path of the archive file to generate that includes a copy of all /// project.asset.json files found. /// </summary> public string ProjectAssetsJsonArchiveFile { get; set; } public override bool Execute() { DateTime startTime = DateTime.Now; Log.LogMessage(MessageImportance.High, "Writing package usage data..."); string[] projectDirectoriesOutsideRoot = ProjectDirectories.NullAsEmpty() .Where(dir => !dir.StartsWith(RootDir, StringComparison.Ordinal)) .ToArray(); if (projectDirectoriesOutsideRoot.Any()) { throw new ArgumentException( $"All ProjectDirectories must be in RootDir '{RootDir}', but found " + string.Join(", ", projectDirectoriesOutsideRoot)); } Log.LogMessage(MessageImportance.Low, "Finding set of RIDs..."); string[] possibleRids = PlatformsRuntimeJsonFiles.NullAsEmpty() .SelectMany(ReadRidsFromRuntimeJson) .Distinct() .ToArray(); Log.LogMessage(MessageImportance.Low, "Reading package identities..."); PackageIdentity[] restored = RestoredPackageFiles.NullAsEmpty() .Select(ReadNuGetPackageInfos.ReadIdentity) .Distinct() .ToArray(); PackageIdentity[] tarballPrebuilt = TarballPrebuiltPackageFiles.NullAsEmpty() .Select(ReadNuGetPackageInfos.ReadIdentity) .Distinct() .ToArray(); PackageIdentity[] referencePackages = ReferencePackageFiles.NullAsEmpty() .Select(ReadNuGetPackageInfos.ReadIdentity) .Distinct() .ToArray(); PackageIdentity[] sourceBuilt = SourceBuiltPackageFiles.NullAsEmpty() .Select(ReadNuGetPackageInfos.ReadIdentity) .Distinct() .ToArray(); IEnumerable<PackageIdentity> prebuilt = restored.Except(sourceBuilt).Except(referencePackages); PackageIdentity[] toCheck = NuGetPackageInfos.NullAsEmpty() .Select(item => new PackageIdentity( item.GetMetadata("PackageId"), NuGetVersion.Parse(item.GetMetadata("PackageVersion")))) .Concat(prebuilt) .ToArray(); Log.LogMessage(MessageImportance.Low, "Finding project.assets.json files..."); string[] assetFiles = Directory .GetFiles(RootDir, "project.assets.json", SearchOption.AllDirectories) .Select(GetPathRelativeToRoot) .Except(IgnoredProjectAssetsJsonFiles.NullAsEmpty().Select(GetPathRelativeToRoot)) .ToArray(); if (!string.IsNullOrEmpty(ProjectAssetsJsonArchiveFile)) { Log.LogMessage(MessageImportance.Low, "Archiving project.assets.json files..."); Directory.CreateDirectory(Path.GetDirectoryName(ProjectAssetsJsonArchiveFile)); using (var projectAssetArchive = new ZipArchive( File.Open( ProjectAssetsJsonArchiveFile, FileMode.Create, FileAccess.ReadWrite), ZipArchiveMode.Create)) { // Only one entry can be open at a time, so don't do this during the Parallel // ForEach later. foreach (var relativePath in assetFiles) { using (var stream = File.OpenRead(Path.Combine(RootDir, relativePath))) using (Stream entryWriter = projectAssetArchive .CreateEntry(relativePath, CompressionLevel.Optimal) .Open()) { stream.CopyTo(entryWriter); } } } } Log.LogMessage(MessageImportance.Low, "Reading usage info..."); var usages = new ConcurrentBag<Usage>(); Parallel.ForEach( assetFiles, assetFile => { JObject jObj; using (var file = File.OpenRead(Path.Combine(RootDir, assetFile))) using (var reader = new StreamReader(file)) using (var jsonReader = new JsonTextReader(reader)) { jObj = (JObject)JToken.ReadFrom(jsonReader); } var properties = new HashSet<string>( jObj.SelectTokens("$.targets.*").Children() .Concat(jObj.SelectToken("$.libraries")) .Select(t => ((JProperty)t).Name) .Distinct(), StringComparer.OrdinalIgnoreCase); var directDependencies = jObj.SelectTokens("$.project.frameworks.*.dependencies").Children().Select(dep => new { name = ((JProperty)dep).Name, target = dep.SelectToken("$..target")?.ToString(), version = VersionRange.Parse(dep.SelectToken("$..version")?.ToString()), autoReferenced = dep.SelectToken("$..autoReferenced")?.ToString() == "True", }) .ToArray(); foreach (var identity in toCheck .Where(id => properties.Contains(id.Id + "/" + id.Version.OriginalVersion))) { var directDependency = directDependencies?.FirstOrDefault( d => d.name == identity.Id && d.version.Satisfies(identity.Version)); usages.Add(Usage.Create( assetFile, identity, directDependency != null, directDependency?.autoReferenced == true, possibleRids)); } }); Log.LogMessage(MessageImportance.Low, "Searching for unused packages..."); foreach (PackageIdentity restoredWithoutUsagesFound in toCheck.Except(usages.Select(u => u.PackageIdentity))) { usages.Add(Usage.Create( null, restoredWithoutUsagesFound, false, false, possibleRids)); } // Packages that were included in the tarball as prebuilts, but weren't even restored. PackageIdentity[] neverRestoredTarballPrebuilts = tarballPrebuilt .Except(restored) .ToArray(); Log.LogMessage(MessageImportance.Low, $"Writing data to '{DataFile}'..."); var data = new UsageData { CreatedByRid = TargetRid, Usages = usages.ToArray(), NeverRestoredTarballPrebuilts = neverRestoredTarballPrebuilts, ProjectDirectories = ProjectDirectories ?.Select(GetPathRelativeToRoot) .ToArray() }; Directory.CreateDirectory(Path.GetDirectoryName(DataFile)); File.WriteAllText(DataFile, data.ToXml().ToString()); Log.LogMessage( MessageImportance.High, $"Writing package usage data... done. Took {DateTime.Now - startTime}"); return !Log.HasLoggedErrors; } private string GetPathRelativeToRoot(string path) { if (path.StartsWith(RootDir)) { return path.Substring(RootDir.Length).Replace(Path.DirectorySeparatorChar, '/'); } throw new ArgumentException($"Path '{path}' is not within RootDir '{RootDir}'"); } private static string[] ReadRidsFromRuntimeJson(string path) { var root = JObject.Parse(File.ReadAllText(path)); return root["runtimes"] .Values<JProperty>() .Select(o => o.Name) .ToArray(); } } }