05a0540476
- Upgraded `.NET` SDK from 9.0.100 to 10.0.100. - Updated `NuGetToolVersion` to 7.0.0. - Adjusted version increment logic for negative delta handling. - Added new framework dependencies for improved compatibility and functionality. - Minor code adjustments and updated generated files.
472 lines
22 KiB
C#
472 lines
22 KiB
C#
using System.Collections;
|
|
using System.Diagnostics;
|
|
using Renci.SshNet;
|
|
using Spectre.Console;
|
|
using aeqw89.xml.ProjectFile;
|
|
|
|
namespace aeqw89.tools.Publish;
|
|
|
|
/*
|
|
* Structure of the program:
|
|
* - publish (executable)
|
|
* - overwrite
|
|
* - destinations
|
|
* - flags
|
|
* - increment
|
|
* - patch|minor|major
|
|
* - destinations
|
|
* - flags
|
|
* e.g. publish overwrite|increment [patch|minor|major] destinations [flags]
|
|
*/
|
|
|
|
public static class Program {
|
|
public static Mode Mode { get; set; }
|
|
public static IncrementTarget? Target { get; set; }
|
|
public static string[] Destinations { get; set; }
|
|
public static Dictionary<string, string[]> Flags { get; set; }
|
|
public static bool Verbose { get; set; } = false;
|
|
|
|
public static void ReadArgs(string[] args) {
|
|
if (args.Length < 1) {
|
|
ShowError(Exceptions.missing_mode.EscapeMarkup());
|
|
ShowHelp();
|
|
return;
|
|
}
|
|
|
|
Mode = args[0] switch {
|
|
"overwrite" => Mode.Overwrite,
|
|
"increment" => Mode.Increment,
|
|
_ => (Mode)(-1)
|
|
};
|
|
|
|
if (Mode == (Mode)(-1)) {
|
|
ShowError(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup());
|
|
ShowHelp();
|
|
return;
|
|
}
|
|
|
|
if (args.Length < 2) {
|
|
if (Mode == Mode.Increment)
|
|
ShowError(Exceptions.missing_increment_target.EscapeMarkup());
|
|
else if (Mode == Mode.Overwrite)
|
|
ShowError(Exceptions.missing_destinations.EscapeMarkup());
|
|
ShowHelp();
|
|
return;
|
|
}
|
|
|
|
Destinations = args[1..];
|
|
Flags = [];
|
|
if (Mode == Mode.Increment) {
|
|
if (args.Length < 3) {
|
|
ShowError(Exceptions.missing_destinations.EscapeMarkup());
|
|
ShowHelp();
|
|
return;
|
|
}
|
|
|
|
Destinations = args[2..];
|
|
|
|
Target = args[1] switch {
|
|
"patch" => IncrementTarget.Patch,
|
|
"minor" => IncrementTarget.Minor,
|
|
"major" => IncrementTarget.Major,
|
|
_ => (IncrementTarget)(-1)
|
|
};
|
|
|
|
if (Target == (IncrementTarget)(-1)) {
|
|
ShowError(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup());
|
|
ShowHelp();
|
|
return;
|
|
}
|
|
}
|
|
|
|
string? firstFlag = Destinations.FirstOrDefault(x => x.StartsWith('-'));
|
|
if (firstFlag == null) return;
|
|
string[] flags = Destinations.SkipWhile(x => x != firstFlag).ToArray();
|
|
Flags = ReadFlags(flags);
|
|
Destinations = Destinations.TakeWhile(x => x != firstFlag).ToArray();
|
|
Verbose = Flags.ContainsKey("--verbose") || Flags.ContainsKey("-v");
|
|
}
|
|
|
|
private static Dictionary<string, string[]> ReadFlags(string[] flags) {
|
|
Dictionary<string, string[]> result = [];
|
|
List<string> collected = [];
|
|
string lastKey = flags[0];
|
|
if (flags.Length == 1)
|
|
result[lastKey] = [];
|
|
foreach (var flag in flags.Skip(1)) {
|
|
if (flag.StartsWith('-')) {
|
|
result[lastKey] = collected.ToArray();
|
|
collected = [];
|
|
lastKey = flag;
|
|
} else
|
|
collected.Add(flag);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static async Task Main(string[] args) {
|
|
ReadArgs(args);
|
|
|
|
string packageId = "";
|
|
string version = "";
|
|
|
|
var result = AnsiConsole.Status()
|
|
.Spinner(Spinner.Known.Dots)
|
|
.Start<bool>("Preparing project", ctx => {
|
|
ctx.Status = "Locating project file";
|
|
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) {
|
|
ShowError(error.EscapeMarkup());
|
|
return false;
|
|
}
|
|
|
|
packageId = projectFile.GetPackageId();
|
|
|
|
try {
|
|
projectFile.Backup();
|
|
if (Verbose)
|
|
AnsiConsole.WriteLine(
|
|
$"Created project file backup at {projectFile.GetDefaultBackupLocation()}");
|
|
|
|
ctx.Status = "Repairing project file";
|
|
if (!Flags.ContainsKey("--skip-repair"))
|
|
if (!projectFile.TryRepair(out error)) {
|
|
ShowError(error.EscapeMarkup());
|
|
projectFile.Restore();
|
|
return false;
|
|
}
|
|
|
|
if (Mode == Mode.Increment && !Flags.ContainsKey("--simulate")) {
|
|
int delta = 1;
|
|
if (Flags.TryGetValue("--delta", out var deltaStrings)) {
|
|
if (deltaStrings.Length != 1) {
|
|
ShowError(Exceptions.flag_parameter_length_incorrect.EscapeMarkup(), "--delta", 1,
|
|
deltaStrings.Length);
|
|
projectFile.Restore();
|
|
ShowHelp();
|
|
return false;
|
|
}
|
|
|
|
if (!int.TryParse(deltaStrings[0], out delta)) {
|
|
ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0, nameof(Int32),
|
|
deltaStrings[0]);
|
|
projectFile.Restore();
|
|
ShowHelp();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ctx.Status = "Updating version";
|
|
var version = projectFile.GetVersion();
|
|
version = ChangeVersion(version,
|
|
Target == IncrementTarget.Patch ? delta : -1,
|
|
Target == IncrementTarget.Minor ? delta : -1,
|
|
Target == IncrementTarget.Major ? delta : -1,
|
|
(x, y) => y < 0 ? x : x + y);
|
|
|
|
projectFile.SetVersion(version);
|
|
}
|
|
}
|
|
catch (Exception e) {
|
|
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
|
|
projectFile.Restore();
|
|
return false;
|
|
}
|
|
|
|
version = projectFile.GetVersion();
|
|
|
|
if (!Flags.ContainsKey("--simulate")) {
|
|
try {
|
|
var packageReferences = projectFile.GetPackageReferences();
|
|
foreach (var reference in packageReferences.Where(x => !projectFile.IsTransitive(x)))
|
|
projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All);
|
|
foreach (var reference in packageReferences.Where(x => projectFile.IsTransitive(x)))
|
|
projectFile.RemovePackage(reference);
|
|
|
|
HashSet<string> visited = [];
|
|
var projectReferences = new Queue<Item>(projectFile.GetProjectReferences().Cast<Item>());
|
|
while (projectReferences.Count != 0) {
|
|
var reference = projectReferences.Dequeue();
|
|
visited.Add(reference.Include);
|
|
|
|
if (Verbose)
|
|
AnsiConsole.WriteLine($"Processing project reference {reference.Include} out of {visited.Count} so far");
|
|
|
|
projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All);
|
|
string pathToReferencedProjectFile = projectFile.GetAbsoluteIncludePath(reference);
|
|
if (!ProjectFile.TryLoad(pathToReferencedProjectFile, out var referencedProjectFile,
|
|
out error)) {
|
|
ShowError(error.EscapeMarkup());
|
|
projectFile.Restore();
|
|
return false;
|
|
}
|
|
|
|
var referencedPackageReferences = referencedProjectFile.GetPackageReferences();
|
|
foreach (var package in referencedPackageReferences) {
|
|
if (Verbose)
|
|
AnsiConsole.WriteLine($"Hoisting package {package.Include} from {pathToReferencedProjectFile}");
|
|
var hoisted = projectFile.AddPackage(package);
|
|
projectFile.SetTransitive(hoisted, true);
|
|
projectFile.SetPrivateAssets(hoisted, PrivateAssetsValue.None);
|
|
referencedProjectFile.SetPrivateAssets(package, PrivateAssetsValue.All);
|
|
}
|
|
|
|
var referencedProjectReferences = referencedProjectFile.GetProjectReferences();
|
|
foreach (var project in referencedProjectReferences) {
|
|
if (!visited.Contains(project.Include))
|
|
projectReferences.Enqueue(project);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
|
|
projectFile.Restore();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
projectFile.Save();
|
|
return true;
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
var outDir = Path.GetRandomFileName();
|
|
result = AnsiConsole.Status()
|
|
.Spinner(Spinner.Known.Dots)
|
|
.Start<bool>("Creating package with 'dotnet pack' ", ctx => {
|
|
var p = Process.Start(new ProcessStartInfo() {
|
|
FileName = "dotnet",
|
|
Arguments = $"pack -o {outDir}",
|
|
WorkingDirectory = Environment.CurrentDirectory,
|
|
UseShellExecute = Verbose,
|
|
RedirectStandardOutput = !Verbose,
|
|
RedirectStandardError = !Verbose
|
|
});
|
|
p?.WaitForExit();
|
|
return p?.ExitCode == 0;
|
|
});
|
|
|
|
if (!result) {
|
|
ShowError(Exceptions.dotnet_pack_failure.EscapeMarkup());
|
|
return;
|
|
}
|
|
|
|
var package = Directory.GetFiles(outDir, "*.nupkg").FirstOrDefault();
|
|
if (package == null) {
|
|
ShowError(Exceptions.generic_error.EscapeMarkup());
|
|
return;
|
|
}
|
|
|
|
var inMemory = await File.ReadAllBytesAsync(package);
|
|
var size = new FileInfo(package).Length;
|
|
const long bufferSize = 80 * 1024; // 80 KB
|
|
try {
|
|
await AnsiConsole.Progress()
|
|
.AutoClear(true)
|
|
.HideCompleted(true)
|
|
.Columns(new ProgressColumn[] {
|
|
new TaskDescriptionColumn(),
|
|
new ProgressBarColumn()
|
|
.RemainingStyle(Style.Parse("dim gray slowblink"))
|
|
.CompletedStyle(Style.Parse("green strikethrough"))
|
|
.FinishedStyle("green strikethrough"),
|
|
new DownloadedColumn(),
|
|
new RemainingTimeColumn(),
|
|
new TransferSpeedColumn(),
|
|
})
|
|
.StartAsync(async ctx => {
|
|
await Parallel.ForEachAsync(Destinations, new ParallelOptions() {
|
|
MaxDegreeOfParallelism = Environment.ProcessorCount,
|
|
}, async (dest, ct) => {
|
|
using var reader = new MemoryStream(inMemory);
|
|
var task = ctx.AddTask(dest, new ProgressTaskSettings() {
|
|
MaxValue = size
|
|
});
|
|
|
|
if (dest.StartsWith("local-")) {
|
|
var name = dest[("local-".Length)..];
|
|
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
name, Path.GetFileName(package));
|
|
if (!Directory.Exists(Path.GetDirectoryName(path)))
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
await using var writer = File.OpenWrite(path);
|
|
var buffer = new byte[bufferSize];
|
|
int read;
|
|
do {
|
|
read = await reader.ReadAsync(buffer, ct);
|
|
writer.Write(buffer, 0, read);
|
|
task.Increment(read);
|
|
} while (read > 0);
|
|
}
|
|
|
|
else if (dest.StartsWith("cloud-")) {
|
|
var name = dest[("cloud-".Length)..];
|
|
var connectionTask = ctx.AddTaskBefore($"Preparing cloud-{name}", new ProgressTaskSettings() {
|
|
MaxValue = 100
|
|
}, task);
|
|
|
|
if (!SshHosts.TryGetHost(name, out var host)) {
|
|
ShowError(Exceptions.cloud_host_not_found.EscapeMarkup(), name);
|
|
return;
|
|
}
|
|
|
|
var connectionInfo = SshHosts.GetConnection(name);
|
|
using var sshClient = new SshClient(connectionInfo);
|
|
if (!sshClient.IsConnected)
|
|
await sshClient.ConnectAsync(ct);
|
|
connectionTask.Increment(33);
|
|
|
|
var winC = sshClient.RunCommand("cmd /c ver");
|
|
var othC = sshClient.RunCommand("uname -s");
|
|
|
|
var os = (winC.ExitStatus, othC.ExitStatus) switch {
|
|
(0, _) => "windows",
|
|
(_, 0) => "linux",
|
|
_ => "unknown"
|
|
};
|
|
|
|
string remoteDirectory;
|
|
string packageFileDirectory;
|
|
if (os == "windows") {
|
|
var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%");
|
|
if (userDirC.ExitStatus != 0) {
|
|
ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, userDirC.Result);
|
|
return;
|
|
}
|
|
|
|
var userDir = userDirC.Result.Trim();
|
|
remoteDirectory = RemotePath.Combine(RemoteOs.Windows,userDir, "dotnet-packages");
|
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, Path.GetFileName(package));
|
|
|
|
var mkdirC = sshClient.RunCommand($"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\"");
|
|
if (mkdirC.ExitStatus != 0) {
|
|
ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, os, mkdirC.Result);
|
|
return;
|
|
}
|
|
}
|
|
else if (os == "linux") {
|
|
var homeDirC = sshClient.RunCommand("printf %s \"$HOME\"");
|
|
if (homeDirC.ExitStatus != 0) {
|
|
ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, homeDirC.Result);
|
|
return;
|
|
}
|
|
var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest
|
|
remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages");
|
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory, Path.GetFileName(package));
|
|
|
|
// Use -p and single quotes to handle spaces safely
|
|
var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'");
|
|
if (mkdirC.ExitStatus != 0) {
|
|
ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, os, mkdirC.Result);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, "Unsupported OS");
|
|
return;
|
|
}
|
|
connectionTask.Increment(33);
|
|
|
|
sshClient.Disconnect();
|
|
|
|
using var client = new SftpClient(connectionInfo);
|
|
if (!client.IsConnected)
|
|
await client.ConnectAsync(ct);
|
|
connectionTask.Increment(33);
|
|
connectionTask.StopTask();
|
|
|
|
await using var writer = client.OpenWrite(packageFileDirectory);
|
|
byte[] buffer = new byte[bufferSize];
|
|
int read;
|
|
do {
|
|
read = await reader.ReadAsync(buffer, ct);
|
|
writer.Write(buffer, 0, read);
|
|
task.Increment(read);
|
|
} while (read > 0);
|
|
}
|
|
|
|
else if (dest == "github") {
|
|
var p = Process.Start(new ProcessStartInfo() {
|
|
FileName = "dotnet",
|
|
Arguments = $"nuget push {package} --source github",
|
|
WorkingDirectory = Environment.CurrentDirectory,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = !Verbose,
|
|
RedirectStandardError = !Verbose
|
|
});
|
|
|
|
if (p == null) {
|
|
ShowError(Exceptions.generic_error.EscapeMarkup());
|
|
}
|
|
|
|
task.Increment(size / 2);
|
|
if (p != null)
|
|
await p.WaitForExitAsync(ct);
|
|
if (p?.ExitCode != 0) {
|
|
ShowError(Exceptions.dotnet_nuget_push_failure, p.ExitCode);
|
|
}
|
|
task.Increment(size / 2);
|
|
}
|
|
|
|
task.StopTask();
|
|
});
|
|
});
|
|
}
|
|
finally {
|
|
try {
|
|
Directory.Delete(outDir, true);
|
|
}
|
|
catch (Exception e) {
|
|
ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(), e.ToString().EscapeMarkup()));
|
|
}
|
|
}
|
|
AnsiConsole.MarkupLine("Completed processing of all destinations.");
|
|
AnsiConsole.MarkupLine("Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId, version);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the version string by applying the specified operation to the major, minor, and patch components of the version.
|
|
/// </summary>
|
|
/// <param name="version">The current version string in the format "major.minor.patch[-tag]".</param>
|
|
/// <param name="patch">The value to apply to the patch component.</param>
|
|
/// <param name="minor">The value to apply to the minor component.</param>
|
|
/// <param name="major">The value to apply to the major component.</param>
|
|
/// <param name="operation">A function that defines the adjustment operation to be performed on each version component.</param>
|
|
/// <returns>A new version string with the updated major, minor, and patch components, preserving any existing tag.</returns>
|
|
/// <exception cref="Exception">Thrown if the version string is not in the correct format.</exception>
|
|
private static string ChangeVersion(string version, int patch, int minor, int major,
|
|
Func<int, int, int> operation) {
|
|
string[] split = version.Split('.');
|
|
if (split.Length != 3) {
|
|
throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version));
|
|
}
|
|
|
|
string tag = "";
|
|
if (split[2].Contains('-')) {
|
|
var split2 = split[2].Split('-');
|
|
split[2] = split2[0];
|
|
tag = "-" + split2[1];
|
|
}
|
|
|
|
if (split.Any(x => !int.TryParse(x, out _)))
|
|
throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version));
|
|
|
|
int[] parsedVersion = split.Select(int.Parse).ToArray();
|
|
|
|
return
|
|
$"{operation(parsedVersion[0], major)}.{operation(parsedVersion[1], minor)}.{operation(parsedVersion[2], patch)}{tag}";
|
|
}
|
|
|
|
private static void ShowError(string message, params object[] args) {
|
|
AnsiConsole.MarkupLine($"[bold red]{message}[/]", args);
|
|
}
|
|
|
|
private static void ShowHelp() {
|
|
AnsiConsole.Markup(("Usage: publish overwrite|increment [patch|minor|major] destinations [flags]\n" +
|
|
"\t if mode: overwrite destinations [flags]\n" +
|
|
"\t if mode: increment patch|minor|major [flags]\n").EscapeMarkup());
|
|
|
|
}
|
|
} |