mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 23:27:21 +00:00
Prepared release v26.11.1 (#1173)
This commit is contained in:
1 parent
9d1768e2f4
commit
520758771b
11 files changed
+339
-66
No files matched your search
+170
-38
@@ -1,25 +1,62 @@
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
|
||||
using Build.Tools;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public static class Pdfium
|
||||
{
|
||||
private static readonly HttpClient CLIENT = new()
|
||||
private const int MAX_DOWNLOAD_ATTEMPTS = 3;
|
||||
|
||||
private static readonly TimeSpan CONNECT_TIMEOUT = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RESPONSE_TIMEOUT = TimeSpan.FromSeconds(60);
|
||||
private static readonly TimeSpan TRANSFER_TIMEOUT = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan RETRY_DELAY = TimeSpan.FromSeconds(5);
|
||||
|
||||
//
|
||||
// Without a handler of its own, HttpClient waits for a connection without any limit, so a single
|
||||
// connection attempt which hangs uses up the whole timeout of the request. That happened twice while
|
||||
// preparing v26.11.1, while curl fetched the same file within a second. The handler limits each
|
||||
// connection attempt, the TLS handshake included, and each download attempt has its own time budget:
|
||||
//
|
||||
private static readonly HttpClient CLIENT = new(new SocketsHttpHandler { ConnectTimeout = CONNECT_TIMEOUT })
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5)
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Pdfium library of the given version into the resources of the Rust runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every failure ends the build: without the library, AI Studio cannot read PDF files, and Tauri would fail
|
||||
/// later with a far less clear message, because its resources include the folder of the library. The download
|
||||
/// is skipped when the library of this version is already installed, which a version marker in the target
|
||||
/// folder of the runtime records. The marker cannot sit next to the library, because Tauri ships everything
|
||||
/// in that folder.
|
||||
/// </remarks>
|
||||
/// <param name="rid">The RID to install the library for.</param>
|
||||
/// <param name="version">The Chromium revision of the Pdfium build, e.g., 7763.</param>
|
||||
/// <param name="offline">True, when the build must not download anything and uses the library already present.</param>
|
||||
public static async Task InstallAsync(RID rid, string version, bool offline)
|
||||
{
|
||||
Console.Write($"- Installing Pdfium {version} for {rid.UserFriendlyName} ...");
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
|
||||
var library = GetLibraryPath(rid);
|
||||
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
|
||||
if (string.IsNullOrWhiteSpace(library.Path) || string.IsNullOrWhiteSpace(pdfiumUrl))
|
||||
{
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: There is no Pdfium library for {rid.UserFriendlyName}.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
|
||||
var versionMarkerPath = Path.Join(cwd, "target", "pdfium.version");
|
||||
var versionMarker = $"{version} {rid.MicrosoftRid}";
|
||||
|
||||
if (offline)
|
||||
{
|
||||
@@ -29,45 +66,90 @@ public static class Pdfium
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist");
|
||||
return;
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: Offline mode is enabled, but '{pdfiumLibTargetPath}' does not exist. Build once without --offline to download it.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(library.Path))
|
||||
if (File.Exists(pdfiumLibTargetPath) && await ReadVersionMarkerAsync(versionMarkerPath) == versionMarker)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library path for {rid.UserFriendlyName}");
|
||||
Console.WriteLine(" already installed, skipping download");
|
||||
return;
|
||||
}
|
||||
|
||||
var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries");
|
||||
var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp");
|
||||
var pdfiumLibArchivePath = library.Path.Replace('\\', '/');
|
||||
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
for (var attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt++)
|
||||
{
|
||||
Console.WriteLine($" failed to download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl}");
|
||||
return;
|
||||
try
|
||||
{
|
||||
await DownloadAndDeployAsync(pdfiumUrl, library.Path.Replace('\\', '/'), pdfiumLibTargetPath, versionMarkerPath, versionMarker);
|
||||
Console.WriteLine(" done.");
|
||||
return;
|
||||
}
|
||||
catch (Exception exception) when (IsTransientFailure(exception))
|
||||
{
|
||||
Console.Write($" failed: {DescribeFailure(exception)}");
|
||||
if (attempt < MAX_DOWNLOAD_ATTEMPTS)
|
||||
{
|
||||
Console.Write($" Retrying in {RETRY_DELAY.TotalSeconds:0} seconds ...");
|
||||
await Task.Delay(RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the library from the downloaded file:
|
||||
//
|
||||
Console.Write(" extracting ...");
|
||||
Directory.CreateDirectory(pdfiumLibTargetDirectory);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"- Error: Could not download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl} in {MAX_DOWNLOAD_ATTEMPTS} attempts.");
|
||||
if (File.Exists(pdfiumLibTargetPath))
|
||||
Console.WriteLine(" To build with the library which is already present, use --offline.");
|
||||
|
||||
var foundLibrary = false;
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the Pdfium archive once, extracts the library from it, and puts the library in place.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A failure which the next attempt might not run into, such as a broken connection or a timeout, leaves this
|
||||
/// method as an exception, which IsTransientFailure recognizes. A failure which every attempt would run into,
|
||||
/// such as an unknown version, ends the build right here.
|
||||
/// </remarks>
|
||||
/// <param name="pdfiumUrl">The address of the Pdfium archive.</param>
|
||||
/// <param name="pdfiumLibArchivePath">The path of the library inside the archive, with forward slashes.</param>
|
||||
/// <param name="pdfiumLibTargetPath">Where to put the library.</param>
|
||||
/// <param name="versionMarkerPath">Where to record which version the library has.</param>
|
||||
/// <param name="versionMarker">The version and RID of the library.</param>
|
||||
private static async Task DownloadAndDeployAsync(string pdfiumUrl, string pdfiumLibArchivePath, string pdfiumLibTargetPath, string versionMarkerPath, string versionMarker)
|
||||
{
|
||||
var pdfiumLibTmpTargetPath = $"{pdfiumLibTargetPath}.{Guid.NewGuid():N}.tmp";
|
||||
var receivedResponse = false;
|
||||
using var attemptTimeout = new CancellationTokenSource(RESPONSE_TIMEOUT);
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await response.Content.ReadAsStreamAsync();
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead, attemptTimeout.Token);
|
||||
receivedResponse = true;
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: The download of {pdfiumUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}. Check the Pdfium version in metadata.txt.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the library from the downloaded file:
|
||||
//
|
||||
attemptTimeout.CancelAfter(TRANSFER_TIMEOUT);
|
||||
Console.Write(" extracting ...");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pdfiumLibTargetPath)!);
|
||||
|
||||
var foundLibrary = false;
|
||||
await using var downloadStream = await response.Content.ReadAsStreamAsync(attemptTimeout.Token);
|
||||
await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress);
|
||||
await using var tarReader = new TarReader(uncompressedStream);
|
||||
|
||||
while (await tarReader.GetNextEntryAsync() is { } entry)
|
||||
while (await tarReader.GetNextEntryAsync(cancellationToken: attemptTimeout.Token) is { } entry)
|
||||
{
|
||||
if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal))
|
||||
continue;
|
||||
@@ -76,35 +158,85 @@ public static class Pdfium
|
||||
break;
|
||||
|
||||
await using var fileStream = File.Create(pdfiumLibTmpTargetPath);
|
||||
await entry.DataStream.CopyToAsync(fileStream);
|
||||
await entry.DataStream.CopyToAsync(fileStream, attemptTimeout.Token);
|
||||
foundLibrary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundLibrary)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive");
|
||||
return;
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: The Pdfium archive {pdfiumUrl} does not contain the library file '{pdfiumLibArchivePath}'.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
//
|
||||
// Deploy the library. The marker goes first, so that it never describes a library
|
||||
// which was only half replaced:
|
||||
//
|
||||
Console.Write(" deploying ...");
|
||||
File.Delete(versionMarkerPath);
|
||||
File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(versionMarkerPath)!);
|
||||
await File.WriteAllTextAsync(versionMarkerPath, versionMarker, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
catch (OperationCanceledException) when (attemptTimeout.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException(receivedResponse
|
||||
? $"The download did not finish within {TRANSFER_TIMEOUT.TotalMinutes:0} minutes."
|
||||
: $"There was no answer within {RESPONSE_TIMEOUT.TotalSeconds:0} seconds.");
|
||||
}
|
||||
catch (OperationCanceledException exception) when (exception.GetBaseException() is TimeoutException)
|
||||
{
|
||||
// The handler reports its connect timeout without the time it waited:
|
||||
throw new TimeoutException($"No connection could be established within {CONNECT_TIMEOUT.TotalSeconds:0} seconds.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(pdfiumLibTmpTargetPath))
|
||||
File.Delete(pdfiumLibTmpTargetPath);
|
||||
}
|
||||
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether another attempt may succeed where a download attempt failed with the given exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception the attempt failed with.</param>
|
||||
/// <returns>True, when the download is worth another attempt.</returns>
|
||||
private static bool IsTransientFailure(Exception exception) => exception is HttpRequestException or OperationCanceledException or TimeoutException or IOException or InvalidDataException;
|
||||
|
||||
/// <summary>
|
||||
/// Describes why a download attempt failed, in one line.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The innermost exception names the actual cause, such as the socket error behind an HttpRequestException
|
||||
/// or the timeout behind a canceled connection attempt, while the outer one often names the host.
|
||||
/// </remarks>
|
||||
/// <param name="exception">The exception the attempt failed with.</param>
|
||||
/// <returns>The reason of the failure.</returns>
|
||||
private static string DescribeFailure(Exception exception)
|
||||
{
|
||||
var innermost = exception.GetBaseException();
|
||||
if (innermost == exception || exception is OperationCanceledException)
|
||||
return innermost.Message;
|
||||
|
||||
return $"{exception.Message} {innermost.Message}";
|
||||
}
|
||||
|
||||
private static async Task<string> ReadVersionMarkerAsync(string versionMarkerPath)
|
||||
{
|
||||
if (!File.Exists(versionMarkerPath))
|
||||
return string.Empty;
|
||||
|
||||
return (await File.ReadAllTextAsync(versionMarkerPath, Encoding.UTF8)).Trim();
|
||||
}
|
||||
|
||||
private static Library GetLibraryPath(RID rid) => rid switch
|
||||
{
|
||||
RID.LINUX_ARM64 or RID.LINUX_X64 => new(Path.Join("lib", "libpdfium.so"), "libpdfium.so"),
|
||||
RID.OSX_ARM64 or RID.OSX_X64 => new(Path.Join("lib", "libpdfium.dylib"), "libpdfium.dylib"),
|
||||
RID.WIN_ARM64 or RID.WIN_X64 => new(Path.Join("bin", "pdfium.dll"), "pdfium.dll"),
|
||||
|
||||
|
||||
_ => new(string.Empty, string.Empty),
|
||||
};
|
||||
|
||||
@@ -115,13 +247,13 @@ public static class Pdfium
|
||||
{
|
||||
RID.LINUX_ARM64 => $"{baseUrl}linux-arm64.tgz",
|
||||
RID.LINUX_X64 => $"{baseUrl}linux-x64.tgz",
|
||||
|
||||
|
||||
RID.OSX_ARM64 => $"{baseUrl}mac-arm64.tgz",
|
||||
RID.OSX_X64 => $"{baseUrl}mac-x64.tgz",
|
||||
|
||||
|
||||
RID.WIN_ARM64 => $"{baseUrl}win-arm64.tgz",
|
||||
RID.WIN_X64 => $"{baseUrl}win-x64.tgz",
|
||||
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,12 +270,35 @@ public sealed partial class UpdateMetadataCommands
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
// The previous release created the changelog with a placeholder in its first line, which
|
||||
// reserved the build number of this release. We fill in the build time below, so check now,
|
||||
// before anything is written, that the placeholder is still there:
|
||||
var changelogPlaceholderHeader = GetFirstLine(await File.ReadAllTextAsync(changelogPath, Encoding.UTF8));
|
||||
var expectedBuildNumber = await ReadBuildNumber() + 1;
|
||||
if (!IsChangelogPlaceholderHeader(changelogPlaceholderHeader, appVersion.VersionText, expectedBuildNumber))
|
||||
{
|
||||
Console.WriteLine($"- Error: The first line of '{Path.GetFileName(changelogPath)}' must be the placeholder '# v{appVersion.VersionText}, build {expectedBuildNumber} (YYYY-MM-xx xx:xx UTC)', but it is '{changelogPlaceholderHeader}'.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureMetainfoReleaseCanBeWritten(appVersion.VersionText);
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
Console.WriteLine($"- Error: {exception.Message}");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
await this.UpdateAppVersion(appVersion);
|
||||
var buildNumber = await this.IncreaseBuildNumber();
|
||||
var buildTime = await this.UpdateBuildTime();
|
||||
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
|
||||
var buildTimeText = await this.UpdateBuildTime();
|
||||
var buildTime = ParseMetadataBuildTime(buildTimeText);
|
||||
await UpdateChangelogHeader(changelogPath, changelogPlaceholderHeader, appVersion.VersionText, buildNumber, buildTime);
|
||||
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTimeText);
|
||||
await this.CreateNextChangelog(buildNumber, appVersion);
|
||||
await WriteMetainfoRelease(appVersion.VersionText, ParseMetadataBuildTime(buildTime));
|
||||
await WriteMetainfoRelease(appVersion.VersionText, buildTime);
|
||||
await this.UpdateProjectCommitHash();
|
||||
await this.UpdateReleaseDependenciesAndLicence();
|
||||
Console.WriteLine();
|
||||
@@ -647,13 +670,8 @@ public sealed partial class UpdateMetadataCommands
|
||||
var nextChangelog = nextChangelogCandidates[0];
|
||||
|
||||
// The release entry itself is written by ApplyRebuildReleaseState, which adds it when it is
|
||||
// missing and moves it to the top otherwise. Here, we only ensure that there is a file to write to:
|
||||
var metainfoPath = GetMetainfoPath();
|
||||
if (!File.Exists(metainfoPath))
|
||||
throw new InvalidOperationException("The AppStream metainfo file does not exist.");
|
||||
|
||||
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
|
||||
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
|
||||
// missing and moves it to the top otherwise. Here, we only ensure that it can be written:
|
||||
await EnsureMetainfoReleaseCanBeWritten(appVersion);
|
||||
|
||||
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Text.Trim();
|
||||
if (!GitCommitHashRegex().IsMatch(headCommitHash))
|
||||
@@ -731,13 +749,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
const string RELEASE_INDENT = " ";
|
||||
|
||||
var metainfoPath = GetMetainfoPath();
|
||||
if (!File.Exists(metainfoPath))
|
||||
throw new InvalidOperationException("The AppStream metainfo file does not exist.");
|
||||
|
||||
var metainfo = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8);
|
||||
if (!ReleasesStartRegex().IsMatch(metainfo))
|
||||
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
|
||||
|
||||
var metainfo = await ReadMetainfo();
|
||||
var changelogEntries = await ReadChangelogEntries(appVersion);
|
||||
|
||||
// Drop any earlier entry of this version, so that the version stays unique and moves to the top.
|
||||
@@ -771,6 +783,34 @@ public sealed partial class UpdateMetadataCommands
|
||||
Console.WriteLine($"- Updated the AppStream metainfo for v{appVersion}, released on {releaseDate}, with {changelogEntries.Count} changelog entries.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks everything WriteMetainfoRelease needs for the given version, without writing anything.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Preparing a release writes the AppStream entry last, after the metadata and the changelogs were changed. When
|
||||
/// it failed only there, e.g., because the changelog has no entry yet, it would leave a half-prepared release
|
||||
/// behind. Hence, the commands call this before they write anything.
|
||||
/// </remarks>
|
||||
/// <param name="appVersion">The version whose release entry will be written.</param>
|
||||
private static async Task EnsureMetainfoReleaseCanBeWritten(string appVersion)
|
||||
{
|
||||
await ReadMetainfo();
|
||||
await ReadChangelogEntries(appVersion);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadMetainfo()
|
||||
{
|
||||
var metainfoPath = GetMetainfoPath();
|
||||
if (!File.Exists(metainfoPath))
|
||||
throw new InvalidOperationException("The AppStream metainfo file does not exist.");
|
||||
|
||||
var metainfo = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8);
|
||||
if (!ReleasesStartRegex().IsMatch(metainfo))
|
||||
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
|
||||
|
||||
return metainfo;
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> ReadChangelogEntries(string appVersion)
|
||||
{
|
||||
var changelogPath = GetChangelogPath(appVersion);
|
||||
@@ -853,6 +893,37 @@ public sealed partial class UpdateMetadataCommands
|
||||
return $"# v{appVersion}, build {buildNumber} ({buildTime.ToUniversalTime():yyyy-MM-dd HH:mm} UTC)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a changelog header is the placeholder which the previous release created for this version and build.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The month of the placeholder is only a guess of when the release would happen, so any month matches.
|
||||
/// </remarks>
|
||||
/// <param name="changelogHeader">The first line of the changelog.</param>
|
||||
/// <param name="appVersion">The version of the release.</param>
|
||||
/// <param name="buildNumber">The build number of the release.</param>
|
||||
/// <returns>True, when the header is the placeholder.</returns>
|
||||
private static bool IsChangelogPlaceholderHeader(string changelogHeader, string appVersion, int buildNumber)
|
||||
{
|
||||
return Regex.IsMatch(changelogHeader, $"^# v{Regex.Escape(appVersion)}, build {buildNumber} \\(20[0-9]{{2}}-[0-9]{{2}}-xx xx:xx UTC\\)$");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the placeholder in the first line of a changelog with the header of the release.
|
||||
/// </summary>
|
||||
/// <param name="changelogPath">The path of the changelog.</param>
|
||||
/// <param name="placeholderHeader">The placeholder, as checked before anything was written.</param>
|
||||
/// <param name="appVersion">The version of the release.</param>
|
||||
/// <param name="buildNumber">The build number of the release.</param>
|
||||
/// <param name="buildTime">The build time of the release.</param>
|
||||
private static async Task UpdateChangelogHeader(string changelogPath, string placeholderHeader, string appVersion, int buildNumber, DateTime buildTime)
|
||||
{
|
||||
var changelogContent = await File.ReadAllTextAsync(changelogPath, Encoding.UTF8);
|
||||
var updatedChangelog = ReplaceExactlyOnce(changelogContent, placeholderHeader, FormatChangelogHeader(appVersion, buildNumber, buildTime));
|
||||
await File.WriteAllTextAsync(changelogPath, updatedChangelog, Environment.UTF8_NO_BOM);
|
||||
Console.WriteLine($"- Updated the header of '{Path.GetFileName(changelogPath)}'.");
|
||||
}
|
||||
|
||||
private static string FormatChangelogLogEntry(string appVersion, int buildNumber, DateTime buildTime, string changelogFilename)
|
||||
{
|
||||
return $"new ({buildNumber}, \"v{appVersion}, build {buildNumber} ({buildTime.ToUniversalTime():yyyy-MM-dd HH:mm} UTC)\", \"{changelogFilename}\"),";
|
||||
@@ -1263,6 +1334,13 @@ public sealed partial class UpdateMetadataCommands
|
||||
return new(sb.ToString(), process.ExitCode);
|
||||
}
|
||||
|
||||
private static async Task<int> ReadBuildNumber()
|
||||
{
|
||||
const int BUILD_NUMBER_INDEX = 2;
|
||||
var lines = await File.ReadAllLinesAsync(Environment.GetMetadataPath(), Encoding.UTF8);
|
||||
return int.Parse(lines[BUILD_NUMBER_INDEX]);
|
||||
}
|
||||
|
||||
private async Task<int> IncreaseBuildNumber()
|
||||
{
|
||||
const int BUILD_NUMBER_INDEX = 2;
|
||||
|
||||
Reference in new issue
Block a user