Skip to content

Commit e86ae78

Browse files
committed
Generation of third-party notices.
1 parent c339c03 commit e86ae78

14 files changed

Lines changed: 907 additions & 233 deletions

src/PostSharp.Engineering.BuildTools/AppExtensions.cs

Lines changed: 212 additions & 210 deletions
Large diffs are not rendered by default.
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
using Microsoft.Extensions.FileSystemGlobbing;
4+
using PostSharp.Engineering.BuildTools.Build;
5+
using PostSharp.Engineering.BuildTools.Build.Model;
6+
using System.Net;
7+
using System.Text.RegularExpressions;
8+
9+
namespace PostSharp.Engineering.BuildTools.BillOfMaterials;
10+
11+
using System;
12+
using System.Collections.Generic;
13+
using System.IO;
14+
using System.Linq;
15+
using System.Net.Http;
16+
using System.Text.Json.Nodes;
17+
using System.Threading.Tasks;
18+
19+
internal static class DependencyWalker
20+
{
21+
private static readonly DependentPackageInfoOverride[] _defaultDependentPackageInfoOverrides =
22+
[
23+
new() { Name = "ComparerExtensions", License = "Public domain" },
24+
new() { Name = "ILRepack", License = "Apache-2.0", UsageKind = DependentPackageUsageKind.Private },
25+
new() { Name = "LibGit2Sharp", License = "MIT" },
26+
new() { Name = "LINQPad", License = "Proprietary" },
27+
new() { Name = "xunit", License = "Apache-2.0", RepositoryUrl = "https://github.com/xunit/xunit" },
28+
29+
// Analyzers are set as Private unless they flow to the consumer.
30+
new() { Name = "StyleCop.Analyzers", UsageKind = DependentPackageUsageKind.Private },
31+
new() { Name = "xunit.analyzers", UsageKind = DependentPackageUsageKind.Private }
32+
];
33+
34+
public static readonly DependentPackageExclusion[] DefaultDependentPackageExclusions =
35+
[
36+
new( "System.", "System" ),
37+
new( "Microsoft.", "System" ),
38+
new( "NETStandard.", "System" ),
39+
new( "Runtime.", "System" )
40+
];
41+
42+
private static IReadOnlyList<PackageDependencyInfo> GetPackageDependencies( BuildContext context )
43+
{
44+
var defaultConfiguration = context.Product.ReadDefaultConfiguration( context ) ?? BuildConfiguration.Debug;
45+
46+
var list = new List<PackageDependencyInfo>();
47+
48+
var depsFiles = new List<FilePatternMatch>();
49+
50+
var depsFilesPattern = Pattern.Create( "**/$(Configuration)/**/*.deps.json" )
51+
.Remove( "**/tests/*.deps.json" )
52+
.Remove( "eng/**/*.deps.json" )
53+
.Append( context.Product.ConsumableDepsFiles );
54+
55+
if ( !depsFilesPattern.TryGetFiles(
56+
context.RepoDirectory,
57+
new BuildInfo( null, defaultConfiguration, context.Product, null ),
58+
depsFiles ) )
59+
{
60+
return [];
61+
}
62+
63+
foreach ( var depsFile in depsFiles )
64+
{
65+
context.Console.WriteMessage( $"Processing {depsFile.Path}..." );
66+
var projectName = Path.GetFileName( depsFile.Path ).Replace( ".deps.json", "", StringComparison.OrdinalIgnoreCase );
67+
68+
try
69+
{
70+
var depsContent = JsonNode.Parse( File.ReadAllText( depsFile.Path ) );
71+
var libraries = depsContent?["libraries"]?.AsObject();
72+
73+
if ( libraries != null )
74+
{
75+
foreach ( var library in libraries )
76+
{
77+
if ( library.Value == null )
78+
{
79+
continue;
80+
}
81+
82+
var type = library.Value["type"]?.ToString();
83+
84+
if ( type != "package" )
85+
{
86+
continue;
87+
}
88+
89+
var parts = library.Key.Split( '/' );
90+
91+
if ( parts.Length == 2 )
92+
{
93+
var packageName = parts[0];
94+
var version = parts[1];
95+
96+
list.Add( new PackageDependencyInfo( projectName, packageName, version ) );
97+
}
98+
}
99+
}
100+
}
101+
catch ( Exception ex )
102+
{
103+
context.Console.WriteWarning( $"Failed to process {depsFile}. Exception: {ex.Message} Skipping." );
104+
}
105+
}
106+
107+
return list;
108+
}
109+
110+
private static async Task<IReadOnlyCollection<PackageInfo>> GetPackageInfoAsync( BuildContext context, IReadOnlyList<PackageDependencyInfo> dependencies )
111+
{
112+
var httpClientHandler = new HttpClientHandler
113+
{
114+
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli
115+
};
116+
117+
using var httpClient = new HttpClient( httpClientHandler );
118+
var packageInfoMap = new Dictionary<string, PackageInfo>();
119+
120+
foreach ( var dependency in dependencies )
121+
{
122+
var packageName = dependency.PackageName;
123+
var version = dependency.PackageVersion;
124+
125+
var projectUsageInfo = context.Product.ProjectUsages
126+
.LastOrDefault( x => Regex.IsMatch( dependency.ProjectName, x.Pattern, RegexOptions.IgnoreCase ) );
127+
128+
var usageKind = projectUsageInfo?.Kind ?? DependentPackageUsageKind.Default;
129+
130+
if ( DefaultDependentPackageExclusions
131+
.Concat( context.Product.DependentPackageExclusions )
132+
.Any( exclusion => packageName.StartsWith( exclusion.Namespace, StringComparison.OrdinalIgnoreCase ) ) )
133+
{
134+
continue;
135+
}
136+
137+
if ( !packageInfoMap.TryGetValue( packageName, out var packageInfo ) )
138+
{
139+
packageInfo = new PackageInfo() { Name = packageName };
140+
packageInfoMap[packageName] = packageInfo;
141+
}
142+
143+
var baseUrl = $"https://api.nuget.org/v3/registration5-gz-semver2/{packageName.ToLowerInvariant()}/{version.ToLowerInvariant()}.json";
144+
var attempts = 0;
145+
const int maxAttempts = 10;
146+
147+
if ( !packageInfo.Versions.TryGetValue( version, out var packageVersionInfo ) )
148+
{
149+
while ( attempts <= maxAttempts )
150+
{
151+
attempts++;
152+
153+
try
154+
{
155+
Console.WriteLine( $"Fetching {baseUrl} for version {version}..." );
156+
var versionEntryResponse = await httpClient.GetStringAsync( baseUrl );
157+
var versionEntryJson = JsonNode.Parse( versionEntryResponse );
158+
159+
if ( versionEntryJson == null )
160+
{
161+
context.Console.WriteWarning( $"'{baseUrl}' returned null." );
162+
163+
continue;
164+
}
165+
166+
var catalogueEntryUrl = versionEntryJson["catalogEntry"]?.ToString();
167+
168+
if ( catalogueEntryUrl == null )
169+
{
170+
context.Console.WriteWarning( $"'{baseUrl}': cannot find the catalogueEntry." );
171+
172+
continue;
173+
}
174+
175+
Console.WriteLine( $"Fetching {catalogueEntryUrl} for version {version}..." );
176+
var catelogueEntryResponse = await httpClient.GetStringAsync( catalogueEntryUrl );
177+
var catalogueEntryJson = JsonNode.Parse( catelogueEntryResponse );
178+
179+
packageVersionInfo = new PackageVersionInfo()
180+
{
181+
Version = version,
182+
License = catalogueEntryJson?["licenseExpression"]?.ToString(),
183+
Owners = catalogueEntryJson?["authors"]?.ToString(),
184+
SourceRepository = catalogueEntryJson?["projectUrl"]?.ToString()
185+
};
186+
187+
packageInfo.Versions[version] = packageVersionInfo;
188+
189+
break;
190+
}
191+
catch ( HttpRequestException ex ) when ( ex.StatusCode != HttpStatusCode.NotFound )
192+
{
193+
if ( attempts < maxAttempts )
194+
{
195+
context.Console.WriteWarning(
196+
$"Failed to fetch package information for {packageName} version {version}. Retrying... ({attempts}/{maxAttempts}). Exception: {ex.Message}" );
197+
198+
await Task.Delay( 15000 );
199+
}
200+
else
201+
{
202+
context.Console.WriteError(
203+
$"Failed to fetch package information for {packageName} version {version} after {maxAttempts} attempts. Exception: {ex.Message}" );
204+
205+
goto skipVersion;
206+
}
207+
}
208+
catch ( Exception e )
209+
{
210+
context.Console.WriteError( $"Failed to fetch package information for {packageName} version {version}. Exception: {e.Message}" );
211+
212+
goto skipVersion;
213+
}
214+
}
215+
216+
packageInfoMap[packageName] = packageInfo;
217+
}
218+
219+
if ( packageVersionInfo != null )
220+
{
221+
ApplyPackageOverrides( context, packageName, packageVersionInfo, ref usageKind );
222+
223+
packageVersionInfo!.Usage.Add( usageKind );
224+
225+
if ( usageKind != DependentPackageUsageKind.Private )
226+
{
227+
if ( projectUsageInfo?.PublicFacingPackages == null )
228+
{
229+
packageVersionInfo.UsedBy.Add( dependency.ProjectName );
230+
}
231+
else
232+
{
233+
foreach ( var publicFacingPackage in projectUsageInfo.PublicFacingPackages )
234+
{
235+
packageVersionInfo.UsedBy.Add( publicFacingPackage );
236+
}
237+
}
238+
}
239+
}
240+
241+
skipVersion: ;
242+
}
243+
244+
return packageInfoMap.Values;
245+
}
246+
247+
private static void ApplyPackageOverrides( BuildContext context, string packageName, PackageVersionInfo packageInfo, ref DependentPackageUsageKind usage )
248+
{
249+
var packageOverrides = _defaultDependentPackageInfoOverrides.Concat( context.Product.DependentPackageInfoOverrides )
250+
.Where( o => packageName.StartsWith( o.Name, StringComparison.OrdinalIgnoreCase ) )
251+
.OrderBy( o => o.Name.Length );
252+
253+
foreach ( var packageOverride in packageOverrides )
254+
{
255+
if ( packageOverride.License != null )
256+
{
257+
packageInfo.License = packageOverride.License;
258+
}
259+
260+
if ( packageOverride.RepositoryUrl != null )
261+
{
262+
packageInfo.SourceRepository = packageOverride.RepositoryUrl;
263+
}
264+
265+
if ( packageOverride.UsageKind != null )
266+
{
267+
usage = packageOverride.UsageKind.Value;
268+
}
269+
}
270+
}
271+
272+
public static async Task<IReadOnlyCollection<PackageInfo>> FindDependenciesAsync( BuildContext context )
273+
{
274+
var dependencies = GetPackageDependencies( context );
275+
276+
var packages = await GetPackageInfoAsync( context, dependencies );
277+
278+
return packages;
279+
}
280+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
namespace PostSharp.Engineering.BuildTools.BillOfMaterials;
4+
5+
/// <summary>
6+
/// Represents an override of values retrieved from nuget.org. In case of <see cref="UsageKind"/>,
7+
/// it overrides <see cref="ProjectUsageInfo"/>.
8+
/// </summary>
9+
public record DependentPackageInfoOverride
10+
{
11+
public required string Name { get; init; }
12+
13+
public string? License { get; init; }
14+
15+
public string? RepositoryUrl { get; init; }
16+
17+
public DependentPackageUsageKind? UsageKind { get; init; }
18+
}
19+
20+
public record DependentPackageExclusion( string Namespace, string Justification );
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
namespace PostSharp.Engineering.BuildTools.BillOfMaterials;
4+
5+
public enum DependentPackageUsageKind
6+
{
7+
/// <summary>
8+
/// Flows with the end-product.
9+
/// </summary>
10+
Default,
11+
12+
/// <summary>
13+
/// Development dependency. Used to build the end-user product, but not to run it.
14+
/// </summary>
15+
Development,
16+
17+
/// <summary>
18+
/// Private asset to the referring repo. Not used by the end-user - neither at run time nor at run time.
19+
/// </summary>
20+
Private,
21+
22+
/// <summary>
23+
/// The package is used for reference but is not shipped with the product.
24+
/// </summary>
25+
Reference
26+
}

0 commit comments

Comments
 (0)