3
3
4
4
using System ;
5
5
using System . IO ;
6
+ using System . Linq ;
6
7
using System . Net . Http ;
7
8
using System . Text ;
9
+ using System . Threading ;
8
10
using System . Threading . Tasks ;
9
11
using System . Xml ;
10
12
using System . Xml . Linq ;
11
13
using Microsoft . Extensions . CommandLineUtils ;
14
+ using NuGet . Common ;
15
+ using NuGet . Configuration ;
12
16
using NuGet . Packaging ;
13
- using NuGet . Packaging . Core ;
17
+ using NuGet . Protocol ;
18
+ using NuGet . Protocol . Core . Types ;
19
+ using NuGet . Versioning ;
14
20
15
21
namespace PackageBaselineGenerator
16
22
{
@@ -26,36 +32,57 @@ static void Main(string[] args)
26
32
27
33
private readonly CommandOption _source ;
28
34
private readonly CommandOption _output ;
29
- private readonly CommandOption _feedv3 ;
35
+ private readonly CommandOption _update ;
30
36
31
37
public Program ( )
32
38
{
33
- _source = Option ( "-s|--source <SOURCE>" , "The NuGet v2 source of the package to fetch" , CommandOptionType . SingleValue ) ;
39
+ _source = Option ( "-s|--package- source <SOURCE>" , "The NuGet source of packages to fetch" , CommandOptionType . SingleValue ) ;
34
40
_output = Option ( "-o|--output <OUT>" , "The generated file output path" , CommandOptionType . SingleValue ) ;
35
- _feedv3 = Option ( "--v3 " , "Sources is nuget v3 " , CommandOptionType . NoValue ) ;
41
+ _update = Option ( "-u|--update " , "Regenerate the input (Baseline.xml) file. " , CommandOptionType . NoValue ) ;
36
42
37
43
Invoke = ( ) => Run ( ) . GetAwaiter ( ) . GetResult ( ) ;
38
44
}
39
45
40
46
private async Task < int > Run ( )
41
47
{
42
48
var source = _source . HasValue ( )
43
- ? _source . Value ( )
44
- : "https://www.nuget.org/api/v2/package" ;
45
-
46
- var packageCache = Environment . GetEnvironmentVariable ( "NUGET_PACKAGES" ) != null
47
- ? Environment . GetEnvironmentVariable ( "NUGET_PACKAGES" )
48
- : Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) , ".nuget" , "packages" ) ;
49
+ ? _source . Value ( ) . TrimEnd ( '/' )
50
+ : "https://api.nuget.org/v3/index.json" ;
51
+ if ( _output . HasValue ( ) && _update . HasValue ( ) )
52
+ {
53
+ await Error . WriteLineAsync ( "'--output' and '--update' options must not be used together." ) ;
54
+ return 1 ;
55
+ }
49
56
50
- var tempDir = Path . Combine ( Directory . GetCurrentDirectory ( ) , "obj" , "tmp" ) ;
51
- Directory . CreateDirectory ( tempDir ) ;
57
+ var inputPath = Path . Combine ( Directory . GetCurrentDirectory ( ) , "Baseline.xml" ) ;
58
+ var input = XDocument . Load ( inputPath ) ;
59
+ var packageSource = new PackageSource ( source ) ;
60
+ var providers = Repository . Provider . GetCoreV3 ( ) ; // Get v2 and v3 API support
61
+ var sourceRepository = new SourceRepository ( packageSource , providers ) ;
62
+ if ( _update . HasValue ( ) )
63
+ {
64
+ return await RunUpdateAsync ( inputPath , input , sourceRepository ) ;
65
+ }
52
66
53
- var input = XDocument . Load ( Path . Combine ( Directory . GetCurrentDirectory ( ) , "Baseline.xml" ) ) ;
67
+ var feedType = await sourceRepository . GetFeedType ( CancellationToken . None ) ;
68
+ var feedV3 = feedType == FeedType . HttpV3 ;
69
+ var packageBase = source + "/package" ;
70
+ if ( feedV3 )
71
+ {
72
+ var resources = await sourceRepository . GetResourceAsync < ServiceIndexResourceV3 > ( ) ;
73
+ packageBase = resources . GetServiceEntryUri ( ServiceTypes . PackageBaseAddress ) . ToString ( ) . TrimEnd ( '/' ) ;
74
+ }
54
75
55
76
var output = _output . HasValue ( )
56
77
? _output . Value ( )
57
78
: Path . Combine ( Directory . GetCurrentDirectory ( ) , "Baseline.Designer.props" ) ;
58
79
80
+ var packageCache = Environment . GetEnvironmentVariable ( "NUGET_PACKAGES" ) ??
81
+ Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) , ".nuget" , "packages" ) ;
82
+
83
+ var tempDir = Path . Combine ( Directory . GetCurrentDirectory ( ) , "obj" , "tmp" ) ;
84
+ Directory . CreateDirectory ( tempDir ) ;
85
+
59
86
var baselineVersion = input . Root . Attribute ( "Version" ) . Value ;
60
87
61
88
var doc = new XDocument (
@@ -66,7 +93,6 @@ private async Task<int> Run()
66
93
new XElement ( "AspNetCoreBaselineVersion" , baselineVersion ) ) ) ) ;
67
94
68
95
var client = new HttpClient ( ) ;
69
-
70
96
foreach ( var pkg in input . Root . Descendants ( "Package" ) )
71
97
{
72
98
var id = pkg . Attribute ( "Id" ) . Value ;
@@ -80,25 +106,26 @@ private async Task<int> Run()
80
106
81
107
if ( ! File . Exists ( nupkgPath ) )
82
108
{
83
- var url = _feedv3 . HasValue ( )
84
- ? $ "{ source } /{ id . ToLowerInvariant ( ) } /{ version } /{ id . ToLowerInvariant ( ) } .{ version } .nupkg"
85
- : $ "{ source } /{ id } /{ version } ";
86
- Console . WriteLine ( $ "Downloading { url } ") ;
87
-
88
- var response = await client . GetStreamAsync ( url ) ;
109
+ var url = feedV3 ?
110
+ $ "{ packageBase } /{ id . ToLowerInvariant ( ) } /{ version } /{ id . ToLowerInvariant ( ) } .{ version } .nupkg" :
111
+ $ "{ packageBase } /{ id } /{ version } ";
89
112
90
- using ( var file = File . Create ( nupkgPath ) )
113
+ Console . WriteLine ( $ "Downloading { url } ") ;
114
+ using ( var response = await client . GetStreamAsync ( url ) )
91
115
{
92
- await response . CopyToAsync ( file ) ;
116
+ using ( var file = File . Create ( nupkgPath ) )
117
+ {
118
+ await response . CopyToAsync ( file ) ;
119
+ }
93
120
}
94
121
}
95
122
96
-
97
123
using ( var reader = new PackageArchiveReader ( nupkgPath ) )
98
124
{
99
125
doc . Root . Add ( new XComment ( $ " Package: { id } ") ) ;
100
126
101
- var propertyGroup = new XElement ( "PropertyGroup" ,
127
+ var propertyGroup = new XElement (
128
+ "PropertyGroup" ,
102
129
new XAttribute ( "Condition" , $ " '$(PackageId)' == '{ id } ' ") ,
103
130
new XElement ( "BaselinePackageVersion" , version ) ) ;
104
131
doc . Root . Add ( propertyGroup ) ;
@@ -122,12 +149,181 @@ private async Task<int> Run()
122
149
Encoding = Encoding . UTF8 ,
123
150
Indent = true ,
124
151
} ;
152
+
125
153
using ( var writer = XmlWriter . Create ( output , settings ) )
126
154
{
127
155
doc . Save ( writer ) ;
128
156
}
157
+
129
158
Console . WriteLine ( $ "Generated file in { output } ") ;
159
+
130
160
return 0 ;
131
161
}
162
+
163
+ private async Task < int > RunUpdateAsync (
164
+ string documentPath ,
165
+ XDocument document ,
166
+ SourceRepository sourceRepository )
167
+ {
168
+ var packageMetadataResource = await sourceRepository . GetResourceAsync < PackageMetadataResource > ( ) ;
169
+ var logger = new Logger ( Error , Out ) ;
170
+ var hasChanged = false ;
171
+ using ( var cacheContext = new SourceCacheContext { NoCache = true } )
172
+ {
173
+ var versionAttribute = document . Root . Attribute ( "Version" ) ;
174
+ hasChanged = await TryUpdateVersionAsync (
175
+ versionAttribute ,
176
+ "Microsoft.AspNetCore.App" ,
177
+ packageMetadataResource ,
178
+ logger ,
179
+ cacheContext ) ;
180
+
181
+ foreach ( var package in document . Root . Descendants ( "Package" ) )
182
+ {
183
+ var id = package . Attribute ( "Id" ) . Value ;
184
+ versionAttribute = package . Attribute ( "Version" ) ;
185
+ var attributeChanged = await TryUpdateVersionAsync (
186
+ versionAttribute ,
187
+ id ,
188
+ packageMetadataResource ,
189
+ logger ,
190
+ cacheContext ) ;
191
+
192
+ hasChanged |= attributeChanged ;
193
+ }
194
+ }
195
+
196
+ if ( hasChanged )
197
+ {
198
+ await Out . WriteLineAsync ( $ "Updating { documentPath } .") ;
199
+
200
+ var settings = new XmlWriterSettings
201
+ {
202
+ Async = true ,
203
+ CheckCharacters = true ,
204
+ CloseOutput = false ,
205
+ Encoding = Encoding . UTF8 ,
206
+ Indent = true ,
207
+ IndentChars = " " ,
208
+ NewLineOnAttributes = false ,
209
+ OmitXmlDeclaration = true ,
210
+ WriteEndDocumentOnClose = true ,
211
+ } ;
212
+
213
+ using ( var stream = File . OpenWrite ( documentPath ) )
214
+ {
215
+ using ( var writer = XmlWriter . Create ( stream , settings ) )
216
+ {
217
+ await document . SaveAsync ( writer , CancellationToken . None ) ;
218
+ }
219
+ }
220
+ }
221
+ else
222
+ {
223
+ await Out . WriteLineAsync ( "No new versions found" ) ;
224
+ }
225
+
226
+ return 0 ;
227
+ }
228
+
229
+ private static async Task < bool > TryUpdateVersionAsync (
230
+ XAttribute versionAttribute ,
231
+ string packageId ,
232
+ PackageMetadataResource packageMetadataResource ,
233
+ ILogger logger ,
234
+ SourceCacheContext cacheContext )
235
+ {
236
+ var searchMetadata = await packageMetadataResource . GetMetadataAsync (
237
+ packageId ,
238
+ includePrerelease : false ,
239
+ includeUnlisted : true , // Microsoft.AspNetCore.DataOrotection.Redis package is not listed.
240
+ sourceCacheContext : cacheContext ,
241
+ log : logger ,
242
+ token : CancellationToken . None ) ;
243
+
244
+ var currentVersion = NuGetVersion . Parse ( versionAttribute . Value ) ;
245
+ var versionRange = new VersionRange (
246
+ currentVersion ,
247
+ new FloatRange ( NuGetVersionFloatBehavior . Patch , currentVersion ) ) ;
248
+
249
+ var latestVersion = versionRange . FindBestMatch (
250
+ searchMetadata . Select ( metadata => metadata . Identity . Version ) ) ;
251
+
252
+ if ( latestVersion == null )
253
+ {
254
+ logger . LogWarning ( $ "Unable to find latest version of '{ packageId } '.") ;
255
+ return false ;
256
+ }
257
+
258
+ var hasChanged = false ;
259
+ if ( latestVersion != currentVersion )
260
+ {
261
+ hasChanged = true ;
262
+ versionAttribute . Value = latestVersion . ToNormalizedString ( ) ;
263
+ }
264
+
265
+ return hasChanged ;
266
+ }
267
+
268
+ private class Logger : ILogger
269
+ {
270
+ private readonly TextWriter _error ;
271
+ private readonly TextWriter _out ;
272
+
273
+ public Logger ( TextWriter error , TextWriter @out )
274
+ {
275
+ _error = error ;
276
+ _out = @out ;
277
+ }
278
+
279
+ public void Log ( LogLevel level , string data )
280
+ {
281
+ switch ( level )
282
+ {
283
+ case LogLevel . Debug :
284
+ LogDebug ( data ) ;
285
+ break ;
286
+ case LogLevel . Error :
287
+ LogError ( data ) ;
288
+ break ;
289
+ case LogLevel . Information :
290
+ LogInformation ( data ) ;
291
+ break ;
292
+ case LogLevel . Minimal :
293
+ LogMinimal ( data ) ;
294
+ break ;
295
+ case LogLevel . Verbose :
296
+ LogVerbose ( data ) ;
297
+ break ;
298
+ case LogLevel . Warning :
299
+ LogWarning ( data ) ;
300
+ break ;
301
+ }
302
+ }
303
+
304
+ public void Log ( ILogMessage message ) => Log ( message . Level , message . Message ) ;
305
+
306
+ public Task LogAsync ( LogLevel level , string data )
307
+ {
308
+ Log ( level , data ) ;
309
+ return Task . CompletedTask ;
310
+ }
311
+
312
+ public Task LogAsync ( ILogMessage message ) => LogAsync ( message . Level , message . Message ) ;
313
+
314
+ public void LogDebug ( string data ) => _out . WriteLine ( $ "Debug: { data } ") ;
315
+
316
+ public void LogError ( string data ) => _error . WriteLine ( $ "Error: { data } ") ;
317
+
318
+ public void LogInformation ( string data ) => _out . WriteLine ( $ "Information: { data } ") ;
319
+
320
+ public void LogInformationSummary ( string data ) => _out . WriteLine ( $ "Summary: { data } ") ;
321
+
322
+ public void LogMinimal ( string data ) => _out . WriteLine ( $ "Minimal: { data } ") ;
323
+
324
+ public void LogVerbose ( string data ) => _out . WriteLine ( $ "Verbose: { data } ") ;
325
+
326
+ public void LogWarning ( string data ) => _out . WriteLine ( $ "Warning: { data } ") ;
327
+ }
132
328
}
133
329
}
0 commit comments