Skip to content

Commit 787240b

Browse files
authored
Merge pull request #26149 from abpframework/gizem/cpm-nightly-support
CPM Support to switch-to-nightly
2 parents 7750001 + e9b3d70 commit 787240b

4 files changed

Lines changed: 213 additions & 11 deletions

File tree

framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public string GetUsageInfo()
3232
sb.AppendLine("");
3333
sb.AppendLine("Options:");
3434
sb.AppendLine("-d|--directory");
35+
sb.AppendLine("-i|--include (optional) comma-separated list of Directory.Packages.props-style files to also update for Central Package Management");
36+
sb.AppendLine("-ep|--exclude-packages (optional) comma-separated list of package ids to never touch in --include files");
3537
sb.AppendLine("");
3638
sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli");
3739

framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private async Task CreateNpmrcFileAsync(string directoryName)
132132

133133
if (!fileContent.Contains(volosoftRegistry))
134134
{
135-
fileContent += volosoftRegistry;
135+
fileContent += Environment.NewLine + volosoftRegistry;
136136
}
137137

138138
File.WriteAllText(fileName, fileContent);

framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Linq;
45
using System.Threading.Tasks;
@@ -66,13 +67,13 @@ public async Task SwitchToNightlyPreview(CommandLineArgs commandLineArgs)
6667

6768
if (solutionPaths.Any())
6869
{
69-
await SwitchSolutionsToNightlyPreview(solutionPaths);
70+
await SwitchSolutionsToNightlyPreview(solutionPaths, commandLineArgs);
7071
}
7172
else
7273
{
7374
var projectPaths = GetProjectPaths(commandLineArgs);
74-
75-
await SwitchProjectsToNightlyPreview(projectPaths);
75+
76+
await SwitchProjectsToNightlyPreview(projectPaths, commandLineArgs);
7677
}
7778
}
7879

@@ -185,13 +186,16 @@ await _npmPackagesUpdater.Update(
185186
}
186187
}
187188

188-
private async Task SwitchProjectsToNightlyPreview(List<string> projects)
189+
private async Task SwitchProjectsToNightlyPreview(List<string> projects, CommandLineArgs commandLineArgs)
189190
{
191+
var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs);
192+
190193
foreach (var project in projects)
191194
{
192195
var folder = Path.GetDirectoryName(project);
196+
var projectFolder = FindSolutionFolder(project) ?? folder;
193197

194-
_packageSourceManager.Add(FindSolutionFolder(project) ?? folder, "ABP Nightly",
198+
_packageSourceManager.Add(projectFolder, "ABP Nightly",
195199
"https://www.myget.org/F/abp-nightly/api/v3/index.json", "Volo.*");
196200

197201
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
@@ -201,11 +205,17 @@ await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
201205
await _npmPackagesUpdater.Update(
202206
folder,
203207
true);
208+
209+
// See SwitchSolutionsToNightlyPreview for the race-avoidance rationale: this
210+
// sequential pass always runs after the per-project UpdateSolutionAsync above.
211+
await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, projectFolder);
204212
}
205213
}
206214

207-
private async Task SwitchSolutionsToNightlyPreview(List<string> solutionPaths)
215+
private async Task SwitchSolutionsToNightlyPreview(List<string> solutionPaths, CommandLineArgs commandLineArgs)
208216
{
217+
var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs);
218+
209219
foreach (var solutionPath in solutionPaths)
210220
{
211221
var solutionFolder = Path.GetDirectoryName(solutionPath);
@@ -232,6 +242,67 @@ await _npmPackagesUpdater.Update(
232242
solutionAngularFolder,
233243
true);
234244
}
245+
246+
// Optional Central Package Management support: only runs when --include is
247+
// explicitly passed, and only after UpdateSolutionAsync's internal parallel
248+
// (Task.WaitAll) per-project update has fully completed, so no --include file
249+
// is ever touched concurrently with anything else.
250+
await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, solutionFolder);
251+
}
252+
}
253+
254+
private async Task<(List<string> IncludeFiles, List<string> ExcludedPackages, string LatestVersionFromMyGet)> ResolveNightlyIncludeContextAsync(
255+
CommandLineArgs commandLineArgs)
256+
{
257+
var includeFiles = GetCommaSeparatedOption(commandLineArgs, Options.Include.Short, Options.Include.Long);
258+
var excludedPackages = GetCommaSeparatedOption(commandLineArgs, Options.Exclude.Short, Options.Exclude.Long);
259+
260+
if (!includeFiles.Any())
261+
{
262+
return (includeFiles, excludedPackages, null);
263+
}
264+
265+
string latestVersionFromMyGet;
266+
try
267+
{
268+
latestVersionFromMyGet = await _nugetPackagesVersionUpdater.GetLatestVersionFromMyGet("Volo.Abp.Core");
269+
}
270+
catch (Exception ex)
271+
{
272+
// Don't let a transient MyGet failure abort the whole switch-to-nightly run
273+
// (source registration / regular PackageReference updates below must still
274+
// proceed for every solution/project) - just skip the --include pass.
275+
Logger.LogWarning(ex, "Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run.");
276+
return (includeFiles, excludedPackages, null);
277+
}
278+
279+
if (latestVersionFromMyGet.IsNullOrWhiteSpace())
280+
{
281+
// No exception was thrown, but MyGet simply has no version for this package yet
282+
// (e.g. not indexed there) - warn so users aren't left wondering why --include did nothing.
283+
Logger.LogWarning("Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run.");
284+
return (includeFiles, excludedPackages, null);
285+
}
286+
287+
return (includeFiles, excludedPackages, latestVersionFromMyGet);
288+
}
289+
290+
private async Task UpdateIncludedCentralPackageFilesAsync(
291+
List<string> includeFiles,
292+
List<string> excludedPackages,
293+
string latestVersionFromMyGet,
294+
string baseFolder)
295+
{
296+
foreach (var includeFile in includeFiles)
297+
{
298+
var resolvedPath = Path.IsPathRooted(includeFile)
299+
? includeFile
300+
: Path.Combine(baseFolder, includeFile);
301+
302+
await _nugetPackagesVersionUpdater.UpdateCentralPackageVersionsAsync(
303+
resolvedPath,
304+
latestVersionFromMyGet,
305+
excludedPackages);
235306
}
236307
}
237308

@@ -285,6 +356,14 @@ private string GetDirectory(CommandLineArgs commandLineArgs)
285356
?? Directory.GetCurrentDirectory();
286357
}
287358

359+
private List<string> GetCommaSeparatedOption(CommandLineArgs commandLineArgs, string shortName, string longName)
360+
{
361+
var raw = commandLineArgs.Options.GetOrNull(shortName, longName);
362+
return raw.IsNullOrWhiteSpace()
363+
? new List<string>()
364+
: raw.Split(',').Select(s => s.Trim()).Where(s => !s.IsNullOrWhiteSpace()).ToList();
365+
}
366+
288367
private string GetSolutionAngularFolder(string solutionFolder)
289368
{
290369
var upperAngularPath = Path.Combine(Directory.GetParent(solutionFolder)?.FullName ?? "", "angular");
@@ -340,5 +419,15 @@ public static class Directory
340419
public const string Short = "d";
341420
public const string Long = "directory";
342421
}
422+
public static class Include
423+
{
424+
public const string Short = "i";
425+
public const string Long = "include";
426+
}
427+
public static class Exclude
428+
{
429+
public const string Short = "ep";
430+
public const string Long = "exclude-packages";
431+
}
343432
}
344433
}

framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ private async Task<string> UpdateVoloPackagesAsync(string content,
217217
}
218218
var currentVersion = versionAttribute.Value;
219219

220-
var isLeptonXPackage = packageId.Contains("LeptonX");
221-
var isStudioPackage = packageId.StartsWith("Volo.Abp.Studio.");
220+
var isLeptonXPackage = IsLeptonXPackage(packageId);
221+
var isStudioPackage = IsStudioPackage(packageId);
222222
if(isLeptonXPackage)
223223
{
224224
//'SemanticVersion.TryParse' can not parse the version if the version contains floating version resolution, such as '*-*'
@@ -366,10 +366,121 @@ void TryUpdatingPackage(string versionToUpdate)
366366
return await Task.FromResult(content);
367367
}
368368

369-
private async Task<string> GetLatestVersionFromMyGet(string packageId)
369+
private static bool IsLeptonXPackage(string packageId) => packageId.Contains("LeptonX");
370+
371+
private static bool IsStudioPackage(string packageId) => packageId.StartsWith("Volo.Abp.Studio.");
372+
373+
internal async Task<string> GetLatestVersionFromMyGet(string packageId)
370374
{
371375
var myGetPack = await _myGetPackageListFinder.GetPackagesAsync();
372376

373377
return myGetPack.Packages.FirstOrDefault(p => p.Id == packageId)?.Versions.LastOrDefault();
374378
}
379+
380+
/// <summary>
381+
/// Updates &lt;PackageVersion Include="Volo.*"&gt; entries in a Central Package Management
382+
/// props file (e.g. Directory.Packages.props) to <paramref name="latestVersionFromMyGet"/>.
383+
/// Regular PackageReference-based updates (UpdateSolutionAsync/UpdateProjectAsync) already
384+
/// skip any PackageReference with no Version attribute (i.e. CPM-managed packages) - this
385+
/// method is the explicit, opt-in counterpart for callers that also want those central
386+
/// versions kept in sync. Not invoked unless a caller (e.g. the switch-to-nightly --include
387+
/// option) explicitly requests it.
388+
/// </summary>
389+
public async Task UpdateCentralPackageVersionsAsync(
390+
string filePath,
391+
string latestVersionFromMyGet,
392+
IEnumerable<string> excludedPackageIds = null)
393+
{
394+
if (!File.Exists(filePath))
395+
{
396+
Logger.LogWarning("--include file not found, skipped: {FilePath}", filePath);
397+
return;
398+
}
399+
400+
if (latestVersionFromMyGet == null)
401+
{
402+
return;
403+
}
404+
405+
var excluded = new HashSet<string>(excludedPackageIds ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
406+
407+
try
408+
{
409+
string fileContent;
410+
Encoding detectedEncoding;
411+
using (var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
412+
using (var sr = new StreamReader(fs, DefaultEncoding, true))
413+
{
414+
fileContent = await sr.ReadToEndAsync();
415+
detectedEncoding = sr.CurrentEncoding;
416+
}
417+
418+
var doc = new XmlDocument { PreserveWhitespace = true };
419+
doc.LoadXml(fileContent);
420+
421+
var packageNodeList = doc.SelectNodes("//PackageVersion[starts-with(@Include, 'Volo.')]");
422+
if (packageNodeList != null)
423+
{
424+
foreach (XmlNode package in packageNodeList)
425+
{
426+
var packageId = package.Attributes?["Include"]?.Value;
427+
if (packageId == null || excluded.Contains(packageId))
428+
{
429+
continue;
430+
}
431+
432+
// LeptonX and Studio packages follow their own, independent version
433+
// stream (see IsLeptonXPackage/IsStudioPackage, also used by
434+
// UpdateVoloPackagesAsync above) - never stamp them with the
435+
// Volo.Abp.Core anchor version, regardless of --exclude-packages.
436+
if (IsLeptonXPackage(packageId) || IsStudioPackage(packageId))
437+
{
438+
continue;
439+
}
440+
441+
var versionAttribute = package.Attributes["Version"];
442+
if (versionAttribute == null)
443+
{
444+
continue;
445+
}
446+
447+
if (versionAttribute.Value != latestVersionFromMyGet)
448+
{
449+
Logger.LogInformation("Updating central package \"{PackageId}\" from v{CurrentVersion} to v{LatestVersion}", packageId, versionAttribute.Value, latestVersionFromMyGet);
450+
versionAttribute.Value = latestVersionFromMyGet;
451+
}
452+
}
453+
}
454+
455+
var updatedXml = doc.OuterXml;
456+
457+
// Write to a temp file in the same directory and atomically swap it in with
458+
// File.Replace, instead of truncating filePath in place - this way a failure
459+
// mid-write (disk full, process killed) never leaves the original file empty
460+
// or partially written; it either stays untouched or is fully replaced.
461+
var tempFilePath = Path.Combine(Path.GetDirectoryName(filePath) ?? string.Empty, Path.GetRandomFileName());
462+
try
463+
{
464+
using (var tempStream = new FileStream(tempFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
465+
using (var sw = new StreamWriter(tempStream, detectedEncoding))
466+
{
467+
await sw.WriteAsync(updatedXml);
468+
await sw.FlushAsync();
469+
}
470+
471+
File.Replace(tempFilePath, filePath, null);
472+
}
473+
finally
474+
{
475+
if (File.Exists(tempFilePath))
476+
{
477+
File.Delete(tempFilePath);
478+
}
479+
}
480+
}
481+
catch (Exception ex)
482+
{
483+
Logger.LogError(ex, "Failed to update central package versions in \"{FilePath}\".", filePath);
484+
}
485+
}
375486
}

0 commit comments

Comments
 (0)