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+ }
0 commit comments