mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 20:50:53 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef74680167 | |||
| 65a40773fa | |||
| 9ddc09ea91 | |||
| c4326a1143 |
@@ -32,4 +32,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
||||||
|
top of the log. Populated from GitHub Actions environment variables when
|
||||||
|
building in CI; empty locally. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ internal static partial class Program
|
|||||||
|
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
|
Log.Info(BuildInfo.Banner);
|
||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
|
LogAppBundleInfo(normalizedEbootPath, image);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||||
@@ -357,6 +359,66 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||||
|
{
|
||||||
|
var executableName = Path.GetFileName(ebootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(executableName))
|
||||||
|
{
|
||||||
|
executableName = "eboot.bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||||
|
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||||
|
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||||
|
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("App bundle info:");
|
||||||
|
builder.AppendLine($"- Display name: {displayName}");
|
||||||
|
builder.AppendLine($"- Version: {version}");
|
||||||
|
builder.AppendLine($"- Title ID: {titleId}");
|
||||||
|
builder.AppendLine($"- Content ID: {contentId}");
|
||||||
|
builder.AppendLine($"- Executable: {executableName}");
|
||||||
|
builder.Append("- Platform: PlayStation 5");
|
||||||
|
Log.Info(builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveContentId(string? bundleRoot)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(bundleRoot))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var candidate in new[]
|
||||||
|
{
|
||||||
|
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||||
|
Path.Combine(bundleRoot, "param.json"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (!File.Exists(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||||
|
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||||
|
{
|
||||||
|
return contentId.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
|
|||||||
@@ -52,14 +52,7 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx)
|
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||||
{
|
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XKRegsFpEpk",
|
Nid = "XKRegsFpEpk",
|
||||||
@@ -172,11 +165,7 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int UnderscoreExit(CpuContext ctx)
|
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||||
{
|
|
||||||
_ = ctx;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -437,4 +426,13 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
||||||
|
{
|
||||||
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int Einval = 22;
|
private const int Einval = 22;
|
||||||
private const int Erange = 34;
|
private const int Erange = 34;
|
||||||
private const int Struncate = 80;
|
private const int Struncate = 80;
|
||||||
|
private const int ClockRealtime = 0;
|
||||||
|
private const int ClockVirtual = 1;
|
||||||
|
private const int ClockProf = 2;
|
||||||
|
private const int ClockMonotonic = 4;
|
||||||
|
private const int ClockUptime = 5;
|
||||||
|
private const int ClockRealtimeFast = 10;
|
||||||
|
private const int ClockMonotonicFast = 12;
|
||||||
private const nuint DefaultLibcHeapAlignment = 16;
|
private const nuint DefaultLibcHeapAlignment = 16;
|
||||||
private const ushort KernelStatModeDirectory = 0x41FF;
|
private const ushort KernelStatModeDirectory = 0x41FF;
|
||||||
private const ushort KernelStatModeRegular = 0x81FF;
|
private const ushort KernelStatModeRegular = 0x81FF;
|
||||||
@@ -1993,23 +2000,55 @@ public static class KernelMemoryCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int ClockGettime(CpuContext ctx)
|
public static int ClockGettime(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
var timespecAddress = ctx[CpuRegister.Rsi];
|
||||||
if (timespecAddress == 0)
|
if (timespecAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
long seconds;
|
||||||
var seconds = now.ToUnixTimeSeconds();
|
long nanoseconds;
|
||||||
var nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
switch (clockId)
|
||||||
if (!ctx.TryWriteUInt64(timespecAddress, unchecked((ulong)seconds)) ||
|
|
||||||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
case ClockRealtime:
|
||||||
|
case ClockRealtimeFast:
|
||||||
|
case ClockVirtual:
|
||||||
|
case ClockProf:
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
seconds = now.ToUnixTimeSeconds();
|
||||||
|
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ClockMonotonic:
|
||||||
|
case ClockMonotonicFast:
|
||||||
|
case ClockUptime:
|
||||||
|
KernelRuntimeCompatExports.GetProcessMonotonicTime(out seconds, out nanoseconds);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
|
||||||
|
{
|
||||||
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -194,13 +194,14 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
GetProcessMonotonicTime(out seconds, out nanoseconds);
|
||||||
seconds = elapsedTicks / Stopwatch.Frequency;
|
|
||||||
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -617,6 +618,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
|
||||||
|
{
|
||||||
|
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
||||||
|
seconds = elapsedTicks / Stopwatch.Frequency;
|
||||||
|
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bnZxYgAFeA0",
|
Nid = "bnZxYgAFeA0",
|
||||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||||
@@ -1469,26 +1477,42 @@ public static class KernelRuntimeCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
const int pageSize = 4096;
|
||||||
Span<byte> one = stackalloc byte[1];
|
const int inlineChunkSize = 64;
|
||||||
for (var i = 0; i < maxLength; i++)
|
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
||||||
|
var length = 0;
|
||||||
|
|
||||||
|
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
||||||
{
|
{
|
||||||
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
var current = address + (ulong)offset;
|
||||||
|
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||||
|
var chunkSize = Math.Min(
|
||||||
|
buffer.Length - length,
|
||||||
|
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
||||||
|
|
||||||
|
if (chunkSize <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (one[0] == 0)
|
var chunk = buffer.Slice(length, chunkSize);
|
||||||
|
if (!ctx.Memory.TryRead(current, chunk))
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nulIndex = chunk.IndexOf((byte)0);
|
||||||
|
if (nulIndex >= 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.Add(one[0]);
|
length += chunkSize;
|
||||||
|
offset += chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||||
@@ -1697,7 +1721,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> stub = stackalloc byte[]
|
ReadOnlySpan<byte> stub = stackalloc byte[]
|
||||||
{
|
{
|
||||||
0x0F, 0x31,
|
0x0F, 0x31,
|
||||||
0x48, 0xC1, 0xE2, 0x20,
|
0x48, 0xC1, 0xE2, 0x20,
|
||||||
@@ -1705,7 +1729,14 @@ public static class KernelRuntimeCompatExports
|
|||||||
0xC3,
|
0xC3,
|
||||||
};
|
};
|
||||||
|
|
||||||
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (byte* src = stub)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build provenance for the running emulator, populated at compile time from
|
||||||
|
/// GitHub Actions environment variables via <c>[AssemblyMetadata]</c> and
|
||||||
|
/// surfaced as a touchHLE-style banner at the top of the log.
|
||||||
|
/// </summary>
|
||||||
|
public static class BuildInfo
|
||||||
|
{
|
||||||
|
private const string ProjectUrl = "https://github.com/par274/sharpemu";
|
||||||
|
private const string CanonicalRepository = "par274/sharpemu";
|
||||||
|
|
||||||
|
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
|
||||||
|
public static string? CommitSha { get; }
|
||||||
|
|
||||||
|
/// <summary>Branch the build was produced from, or <c>null</c>.</summary>
|
||||||
|
public static string? Branch { get; }
|
||||||
|
|
||||||
|
/// <summary>Repository slug (<c>owner/name</c>) the build came from, or <c>null</c>.</summary>
|
||||||
|
public static string? Repository { get; }
|
||||||
|
|
||||||
|
/// <summary>URL of the GitHub Actions workflow run that produced the build, or <c>null</c>.</summary>
|
||||||
|
public static string? WorkflowRunUrl { get; }
|
||||||
|
|
||||||
|
/// <summary>Build configuration (e.g. <c>Debug</c> or <c>Release</c>), or <c>null</c>.</summary>
|
||||||
|
public static string? Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this build is an official release: a Release-configuration CI build
|
||||||
|
/// from the canonical repository, produced by a push to <c>main</c> or a manual
|
||||||
|
/// workflow dispatch (matching the CI <c>release</c> job). All other builds
|
||||||
|
/// (pull requests, forks, feature branches, and local/Debug builds) are
|
||||||
|
/// considered unofficial.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsOfficialRelease { get; }
|
||||||
|
|
||||||
|
static BuildInfo()
|
||||||
|
{
|
||||||
|
var metadata = ReadMetadata();
|
||||||
|
|
||||||
|
CommitSha = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildSha"));
|
||||||
|
if (CommitSha is { Length: > 7 })
|
||||||
|
{
|
||||||
|
CommitSha = CommitSha[..7];
|
||||||
|
}
|
||||||
|
|
||||||
|
Branch = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildBranch"));
|
||||||
|
Repository = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRepository"));
|
||||||
|
Configuration = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildConfiguration"));
|
||||||
|
|
||||||
|
var serverUrl = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildServerUrl"));
|
||||||
|
var runId = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRunId"));
|
||||||
|
if (serverUrl is not null && Repository is not null && runId is not null)
|
||||||
|
{
|
||||||
|
WorkflowRunUrl = $"{serverUrl.TrimEnd('/')}/{Repository}/actions/runs/{runId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventName = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildEventName"));
|
||||||
|
var gitRef = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRef"));
|
||||||
|
|
||||||
|
var isReleaseConfig = string.Equals(Configuration, "Release", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isCanonicalRepo = string.Equals(Repository, CanonicalRepository, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isReleaseTrigger =
|
||||||
|
string.Equals(eventName, "workflow_dispatch", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(string.Equals(eventName, "push", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(gitRef, "refs/heads/main", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
IsOfficialRelease = isReleaseConfig && isCanonicalRepo && isReleaseTrigger && CommitSha is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The multi-line banner, e.g.
|
||||||
|
/// <code>
|
||||||
|
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
|
||||||
|
///
|
||||||
|
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
|
||||||
|
/// </code>
|
||||||
|
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
|
||||||
|
/// local-build line when no CI provenance is present.
|
||||||
|
/// </summary>
|
||||||
|
public static string Banner
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string version;
|
||||||
|
if (CommitSha is null)
|
||||||
|
{
|
||||||
|
version = "UNOFFICIAL";
|
||||||
|
}
|
||||||
|
else if (IsOfficialRelease)
|
||||||
|
{
|
||||||
|
version = CommitSha;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
version = $"UNOFFICIAL {CommitSha}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = $"SharpEmu {version} — {ProjectUrl}";
|
||||||
|
|
||||||
|
if (Branch is null || Repository is null || WorkflowRunUrl is null)
|
||||||
|
{
|
||||||
|
return $"{header}{Environment.NewLine}{Environment.NewLine}Local build (not produced by CI).";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{header}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
|
$"Built from branch \"{Branch}\" of \"{Repository}\" by GitHub Actions workflow run {WorkflowRunUrl}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ReadMetadata()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var assemblies = new[] { Assembly.GetEntryAssembly(), typeof(BuildInfo).Assembly };
|
||||||
|
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
if (assembly is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in assembly.GetCustomAttributes<AssemblyMetadataAttribute>())
|
||||||
|
{
|
||||||
|
if (attribute.Key.StartsWith("SharpEmu.Build", StringComparison.Ordinal) &&
|
||||||
|
!result.ContainsKey(attribute.Key) &&
|
||||||
|
attribute.Value is not null)
|
||||||
|
{
|
||||||
|
result[attribute.Key] = attribute.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user