Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions Obsidian/HakoVault/Minecraft/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ Hako под ARM64 тоже распознаёт ARM64-хост. На ARM64 сн

`MojangCompatibility.Libraries` сохраняет порядок библиотек из манифеста, включая legacy natives.
Суффикс `${arch}` в legacy classifier подменяется на `64`, `32` или `arm64`; `natives-windows` без
суффикса также поддерживается для x86.
суффикса также поддерживается для x86. Все выбранные natives — и старого формата с картой `natives`,
и нового формата 26.3+, где каждый native-jar — отдельная запись с classifier в `name` и
`downloads.artifact` — всегда ставятся на распаковку (`ExtractNatives`), поэтому нативные jar не
попадают в classpath и всегда распаковываются в natives-каталог; excludes берутся из `extract.exclude`,
когда они есть у записи.

`MinecraftInstaller.InstallAsync` пишет только в корень переданной сборки и использует
`InstanceStorage.ContainedPath` этапа хранения для путей и запрета reparse points. Java собирается из
Expand All @@ -84,13 +88,16 @@ runtime manifest raw-файлов. Link-записи отклоняются на
Очистка best-effort и не останавливает установку при ошибках доступа, ввода-вывода или гонках. Это безопасно,
поскольку `InstanceStore` не допускает одновременные установки одного корня.
Клиент и полный JSON версии размещаются в `minecraft/versions/{id}/`, библиотеки — в
`minecraft/libraries/`, legacy natives после безопасной проверки ZIP — в `minecraft/natives/`, logging —
в `minecraft/assets/log_configs/`. Современные native JAR остаются в libraries. Asset objects хранятся
`minecraft/libraries/`, native JAR после безопасной проверки ZIP — в `minecraft/natives/`, а при
шаблоне JVM `java.library.path=${natives_directory}/java` (формат версии 26.3+) — в
`minecraft/natives/java/`; каталог определяется по аргументам версии (`NativePublishDirectory`),
logging — в `minecraft/assets/log_configs/`. Asset objects хранятся
в `minecraft/assets/objects/`; только virtual и `map_to_resources` проверяют имена и создают проверяемые
копии, коллизии Windows-путей без учёта регистра отклоняются. Создаются `minecraft/mods` и папка
ресурспаков (`resourcepacks`, либо `texturepacks` для legacy версии). Все legacy native archives сначала
распаковываются во временную staging-папку и публикуются заменой каталога `minecraft/natives` после
завершения; если переименование staging не удалось, прежний каталог восстанавливается. Отмена и ошибки
ресурспаков (`resourcepacks`, либо `texturepacks` для legacy версии). Все native archives сначала
распаковываются во временную staging-папку и публикуются заменой каталога `minecraft/natives` (или
`minecraft/natives/java`) после завершения; если переименование staging не удалось, прежний каталог
восстанавливается. Отмена и ошибки
распаковки удаляют staging без частично опубликованных файлов.

`Instances/InstallationCoordinator.cs` — app-lifetime FIFO-очередь, одна активная установка, порядок по `CreatedAt`.
Expand Down
39 changes: 35 additions & 4 deletions windows/Hako.Core/Minecraft/MinecraftInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public sealed class MinecraftInstaller(MojangClient client, FabricClient? fabric
private const int MaximumNativeArchiveEntries = 100_000;
private const long MaximumNativeArchiveSize = 1024L * 1024 * 1024;
private const int BufferSize = 81920;
private const string NativeDirectoryJavaTemplate = "${natives_directory}/java";
private static readonly HashSet<string> ReservedDeviceNames = BuildReservedDeviceNames();
private readonly FabricClient _fabricClient = fabricClient ?? new FabricClient();

Expand Down Expand Up @@ -330,7 +331,8 @@ async Task DownloadAndCancelOnErrorAsync(DownloadJob job)
progress?.Report(new InstallationProgress(InstallationProgressStage.PreparingLibraries, totalBytes, totalBytes, jobs.Length, jobs.Length));
if (nativeArchives.Count > 0)
{
await ExtractNativesAsync(nativeArchives.DistinctBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray(), root, cancellationToken)
await ExtractNativesAsync(nativeArchives.DistinctBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray(), root,
NativePublishDirectory(manifest), cancellationToken)
.ConfigureAwait(false);
}

Expand Down Expand Up @@ -591,9 +593,36 @@ private static async Task CopyAtomicallyAsync(
}
}

/// <summary>
/// Versions whose JVM template points java.library.path at <c>${natives_directory}/java</c> (Minecraft 26.3+)
/// publish native libraries into that subdirectory; older versions use the natives folder root.
/// </summary>
private static string NativePublishDirectory(MojangVersionMetadata manifest)
{
const string nativesRoot = "minecraft/natives";
if (manifest.Arguments is not { } arguments)
{
return nativesRoot;
}

foreach (var group in arguments.Values)
{
foreach (var argument in group)
{
if (argument.Values.Any(value => value.Contains(NativeDirectoryJavaTemplate, StringComparison.Ordinal)))
{
return $"{nativesRoot}/java";
}
}
}

return nativesRoot;
}

private static async Task ExtractNativesAsync(
IReadOnlyList<NativeArchive> archives,
string root,
string destinationRelative,
CancellationToken cancellationToken)
{
var stagingRelative = $"minecraft/.hako-natives-{Guid.NewGuid():N}.tmp";
Expand All @@ -608,7 +637,7 @@ private static async Task ExtractNativesAsync(
}

cancellationToken.ThrowIfCancellationRequested();
PublishNativeStaging(stagingPath, root, Directory.Move);
PublishNativeStaging(stagingPath, root, Directory.Move, destinationRelative);
}
finally
{
Expand Down Expand Up @@ -749,7 +778,8 @@ internal static async Task ExtractNativeEntryAsync(
}
}

internal static void PublishNativeStaging(string stagingPath, string root, Action<string, string> moveDirectory)
internal static void PublishNativeStaging(string stagingPath, string root, Action<string, string> moveDirectory,
string destinationRelative = "minecraft/natives")
{
var pending = new Stack<string>();
pending.Push(stagingPath);
Expand All @@ -770,7 +800,8 @@ internal static void PublishNativeStaging(string stagingPath, string root, Actio
}
}

var destination = InstanceStorage.ContainedPath("minecraft/natives", root);
var destination = InstanceStorage.ContainedPath(destinationRelative, root);
CreateDirectory(Path.GetDirectoryName(destinationRelative) ?? "minecraft", root);
var backup = InstanceStorage.ContainedPath($"minecraft/.hako-natives-backup-{Guid.NewGuid():N}.tmp", root);
var destinationMoved = false;
try
Expand Down
4 changes: 2 additions & 2 deletions windows/Hako.Core/Minecraft/MojangClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ public static IReadOnlyList<LibraryInstallation> Libraries(
native,
native.Path,
IsNative: true,
ExtractNatives: library.Extract is not null,
ExtractNatives: true,
ExtractionExcludes: library.Extract?.Exclude ?? []));
mappedGroups.Add(library.NativeGroup);
}
Expand Down Expand Up @@ -528,7 +528,7 @@ public static IReadOnlyList<LibraryInstallation> Libraries(
download,
download.Path,
IsNative: true,
ExtractNatives: candidate.Library.Extract is not null,
ExtractNatives: true,
ExtractionExcludes: candidate.Library.Extract?.Exclude ?? []));
mappedGroups.Add(group.Key);
}
Expand Down
80 changes: 80 additions & 0 deletions windows/Hako.Tests/MinecraftInstallationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,60 @@ public void LegacyWindowsNativeWithoutSuffixIsSupportedOnX86()
Assert.IsTrue(libraries.Single().IsNative);
}

[TestMethod]
public void ClassifiedNativeEntriesWithoutAnExtractSectionAreQueuedForExtraction()
{
var manifest = new MojangVersionMetadata
{
Libraries =
[
new MojangLibrary
{
Name = "org.lwjgl:lwjgl:3.4.3",
Downloads = new MojangLibraryDownloads { Artifact = Download("lwjgl-3.4.3.jar") }
},
new MojangLibrary
{
Name = "org.lwjgl:lwjgl:3.4.3:natives-windows",
Downloads = new MojangLibraryDownloads { Artifact = Download("lwjgl-3.4.3-natives-windows.jar") },
Rules = [new MojangRule { Action = "allow", Os = new MojangOperatingSystem { Name = "windows" } }]
}
]
};

var libraries = MojangCompatibility.Libraries(manifest, GameArchitecture.X64);

Assert.AreEqual(2, libraries.Count);
var native = libraries.Single(library => library.IsNative);
Assert.AreEqual("lwjgl-3.4.3-natives-windows.jar", native.Path);
Assert.IsTrue(native.ExtractNatives);
Assert.IsFalse(native.ExtractionExcludes!.Any());
}

[TestMethod]
public void NativeMapLibrariesExtractEvenWhenTheExtractSectionIsMissing()
{
var manifest = new MojangVersionMetadata
{
Libraries =
[
new MojangLibrary
{
Name = "org.lwjgl:lwjgl:3.3.1",
Natives = new Dictionary<string, string> { ["windows"] = "natives-windows" },
Downloads = new MojangLibraryDownloads
{
Classifiers = new Dictionary<string, MojangDownload> { ["natives-windows"] = Download("lwjgl-natives-windows.jar") }
}
}
]
};

var libraries = MojangCompatibility.Libraries(manifest, GameArchitecture.X64);

Assert.IsTrue(libraries.Single().ExtractNatives);
}

[TestMethod]
[DataRow("8u74-b01", 8)]
[DataRow("1.8.0_202", 8)]
Expand Down Expand Up @@ -778,6 +832,25 @@ public async Task LegacyNativeArchiveHonorsMetaInfExclusion()
Assert.IsTrue(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "org", "test", "native.dll")));
}

[TestMethod]
public async Task ModernNativesPublishIntoTheJavaLibraryPathSubdirectory()
{
using var temporary = new TestDirectory();
var zip = CreateZip("lwjgl.dll", "META-INF/MANIFEST.MF");
var fixture = CreateInstallFixture(
includeAssets: false,
arguments: JvmArguments("-Djava.library.path=${natives_directory}/java", "-Djna.tmpdir=${natives_directory}/jna"),
libraries: [CreateNativeLibrary(zip, ["META-INF/"])],
nativeArchiveBytes: zip);
using var client = new MojangClient(new HttpClient(fixture.Handler));
var installer = new MinecraftInstaller(client);

await installer.InstallAsync(fixture.Prepared, temporary.Path);

Assert.IsTrue(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "java", "lwjgl.dll")));
Assert.IsFalse(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "java", "META-INF", "MANIFEST.MF")));
}

[TestMethod]
public async Task FailureInSecondNativeArchiveDoesNotPublishAnyStagedFiles()
{
Expand Down Expand Up @@ -1017,6 +1090,7 @@ private static InstallFixture CreateInstallFixture(
IReadOnlyList<string>? assetNames = null,
bool virtualAssets = true,
bool mapToResources = true,
Dictionary<string, List<MojangArgument>>? arguments = null,
IReadOnlyCollection<LibraryInstallation>? libraries = null,
byte[]? nativeArchiveBytes = null,
IReadOnlyDictionary<string, byte[]>? downloadPayloadOverrides = null,
Expand Down Expand Up @@ -1076,6 +1150,7 @@ private static InstallFixture CreateInstallFixture(
JavaVersion = new MojangJavaRequirement { Component = "java-runtime-gamma", MajorVersion = 17 },
Downloads = new Dictionary<string, MojangDownload>(StringComparer.OrdinalIgnoreCase) { ["client"] = clientDownload },
Libraries = [],
Arguments = arguments,
AssetIndex = assetReference,
Logging = new Dictionary<string, MojangLoggingConfiguration>
{
Expand Down Expand Up @@ -1173,6 +1248,11 @@ private static byte[] CreateZip(params string[] entryNames)
Size = 7
};

private static Dictionary<string, List<MojangArgument>> JvmArguments(params string[] values) => new(StringComparer.Ordinal)
{
["jvm"] = values.Select(value => new MojangArgument { Value = JsonSerializer.SerializeToElement(value) }).ToList()
};

private sealed record InstallFixture(PreparedInstallation Prepared, PayloadHandler Handler, byte[] ClientBytes);

private sealed class PayloadHandler(
Expand Down
Loading