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
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"ilspycmd": {
"version": "10.1.1.8388",
"version": "11.0.0.9375",
"commands": [
"ilspycmd"
]
Expand Down
4 changes: 2 additions & 2 deletions .config/ilspycmd-compat.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"minimumVersion": "10.1.0.8386",
"maximumVersion": "10.1.1.8388"
"minimumVersion": "11.0.0.9375",
"maximumVersion": "11.0.0.9375"
}
6 changes: 3 additions & 3 deletions INSTALLER-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ against a build that takes twenty minutes and allocates gigabytes.
installer's `Resolve-DotNetPath` (`scripts/install-windows.ps1:336`),
`Find-AllVintageStory` (`:204`), and `Find-ILSpyCmd` (`:523`).
- The ilspycmd pin and accepted range, read from `.config/dotnet-tools.json`
(`10.1.1.8388`) and `.config/ilspycmd-compat.json` (`10.1.0.8386` through
`10.1.1.8388`). The Windows installer already reads both files in
(`11.0.0.9375`) and `.config/ilspycmd-compat.json` (`11.0.0.9375` through
`11.0.0.9375`). The Windows installer already reads both files in
`Get-Pinned-ILSpyVersion` (`:565`) and `Get-Accepted-ILSpyVersionRange` (`:580`).
Core reads them once and both front ends share the result.
- Acquisition: the `dotnet-install` script runner, `dotnet tool install -g
Expand Down Expand Up @@ -275,7 +275,7 @@ integer. `detail` is a human string and carries no contract.
Log:

```json
{"type":"log","level":"info","message":"ilspycmd 10.1.1.8388 accepted"}
{"type":"log","level":"info","message":"ilspycmd 11.0.0.9375 accepted"}
```

`level` is one of `info`, `warn`, `error`.
Expand Down
46 changes: 30 additions & 16 deletions Optimum.Bootstrap.Core.TestSupport/FakeSystemProbe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,34 @@ public sealed class FakeSystemProbe : ISystemProbe

public FakeSystemProbe AddFile(string path, string? content = null)
{
Files.Add(path);
Files.Add(Norm(path));
if (content is not null)
FileContents[path] = content;
FileContents[Norm(path)] = content;
return this;
}

public FakeSystemProbe AddDirectory(string path)
{
Directories.Add(path);
Directories.Add(Norm(path));
return this;
}

public FakeSystemProbe AddSymlink(string path)
{
Symlinks.Add(path);
Symlinks.Add(Norm(path));
return this;
}

public FakeSystemProbe AddNonExecutableFile(string path)
{
Files.Add(path);
NonExecutable.Add(path);
Files.Add(Norm(path));
NonExecutable.Add(Norm(path));
return this;
}

public FakeSystemProbe OnCommand(string exe, string args, string stdout = "", int exitCode = 0)
{
Commands[$"{exe}|{args}"] = new ProcessOutcome(true, exitCode, stdout, string.Empty);
Commands[$"{Norm(exe)}|{args}"] = new ProcessOutcome(true, exitCode, stdout, string.Empty);
return this;
}

Expand All @@ -61,25 +61,39 @@ public FakeSystemProbe OnCommand(string exe, string args, string stdout = "", in

IReadOnlyList<string> ISystemProbe.PathDirectories => Path;

bool ISystemProbe.FileExists(string path) => Files.Contains(path);
bool ISystemProbe.FileExists(string path) => Files.Contains(Norm(path));

bool ISystemProbe.IsExecutable(string path) => Files.Contains(path) && !NonExecutable.Contains(path);
bool ISystemProbe.IsExecutable(string path) => Files.Contains(Norm(path)) && !NonExecutable.Contains(Norm(path));

bool ISystemProbe.DirectoryExists(string path) => Directories.Contains(path);
bool ISystemProbe.DirectoryExists(string path) => Directories.Contains(Norm(path));

bool ISystemProbe.PathExists(string path) =>
Files.Contains(path) || Directories.Contains(path) || Symlinks.Contains(path);
Files.Contains(Norm(path)) || Directories.Contains(Norm(path)) || Symlinks.Contains(Norm(path));

bool ISystemProbe.IsSymbolicLink(string path) => Symlinks.Contains(path);
bool ISystemProbe.IsSymbolicLink(string path) => Symlinks.Contains(Norm(path));

string? ISystemProbe.ReadText(string path) =>
FileContents.TryGetValue(path, out string? content) ? content : null;
FileContents.TryGetValue(Norm(path), out string? content) ? content : null;

IEnumerable<string> ISystemProbe.EnumerateFiles(string directory, string searchPattern) =>
Files.Where(f => System.IO.Path.GetDirectoryName(f) == directory && Matches(f, searchPattern));
Files.Where(f => DirOf(f) == Norm(directory) && Matches(f, searchPattern));

IEnumerable<string> ISystemProbe.EnumerateDirectories(string directory, string searchPattern) =>
Directories.Where(d => System.IO.Path.GetDirectoryName(d) == directory && Matches(d, searchPattern));
Directories.Where(d => DirOf(d) == Norm(directory) && Matches(d, searchPattern));

/// <summary>
/// The fake models a POSIX filesystem with '/' separators. Production code
/// builds candidate paths with <see cref="System.IO.Path.Combine"/>, which on
/// Windows inserts '\'. Normalise both the stored keys and every lookup to '/'
/// so the tests behave identically on Linux CI and a Windows developer box.
/// </summary>
private static string Norm(string path) => path.Replace('\\', '/');

private static string? DirOf(string path)
{
int slash = path.LastIndexOf('/');
return slash <= 0 ? (slash == 0 ? "/" : null) : path[..slash];
}

private static bool Matches(string path, string searchPattern)
{
Expand All @@ -91,7 +105,7 @@ private static bool Matches(string path, string searchPattern)
}

ProcessOutcome ISystemProbe.Run(string executable, IReadOnlyList<string> arguments, TimeSpan timeout) =>
Commands.TryGetValue($"{executable}|{string.Join(' ', arguments)}", out ProcessOutcome outcome)
Commands.TryGetValue($"{Norm(executable)}|{string.Join(' ', arguments)}", out ProcessOutcome outcome)
? outcome
: ProcessOutcome.NotStarted;
}
4 changes: 2 additions & 2 deletions Optimum.Bootstrap.Core.Tests/AcquisitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public void IlspycmdToolArgumentsMatchTheLoggedInvocation()
{
// scripts/tests/install-linux-prerequisites.sh asserts exactly this line.
Assert.Equal(
"tool update -g ilspycmd --version 10.1.1.8388 --allow-downgrade",
string.Join(' ', IlspycmdAcquisition.ToolArguments("10.1.1.8388")));
"tool update -g ilspycmd --version 11.0.0.9375 --allow-downgrade",
string.Join(' ', IlspycmdAcquisition.ToolArguments("11.0.0.9375")));
}

[Fact]
Expand Down
16 changes: 16 additions & 0 deletions Optimum.Bootstrap.Core.Tests/DeployRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ public void DeployRefusesANonEmptyDirectoryWithNoManifest()
Assert.True(File.Exists(Path.Combine(occupied, "someone-elses-file")));
}

[Fact]
public void DeployCleansANonEmptyDirectoryWhenCleanDestinationIsTrue()
{
var probe = SystemProbe.Default;
string package = StagePackage();
string occupied = Path.Combine(_root, "occupied-clean");
Directory.CreateDirectory(occupied);
File.WriteAllText(Path.Combine(occupied, "someone-elses-file"), "x");

DeployResult result = new PackageDeployer(probe).Deploy(new DeployRequest(package, occupied, CleanDestination: true));

Assert.True(result.Ok);
Assert.False(File.Exists(Path.Combine(occupied, "someone-elses-file")));
Assert.True(File.Exists(Path.Combine(occupied, "run.sh")));
}

[Fact]
public void DeployReplacesAnExistingOptimumInstallInPlace()
{
Expand Down
6 changes: 5 additions & 1 deletion Optimum.Bootstrap.Core.Tests/GamePatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ public async Task RealFilesystemRoundTripPatchAndRollback()
string patcherExe = Path.Combine(overlayDir, OperatingSystem.IsWindows() ? "Optimum.Patcher.bat" : "Optimum.Patcher");
if (OperatingSystem.IsWindows())
{
File.WriteAllText(patcherExe, "@echo off\r\necho patched > %~dpnx3\r\nexit /b 0\r\n");
// Mirror the Unix stub: write "patched" to the LAST argument,
// whichever position it is (the lib call passes 3 args, the --api
// call passes 4). A for-loop keeps the final token in %%t.
File.WriteAllText(patcherExe,
"@echo off\r\nset \"target=\"\r\nfor %%t in (%*) do set \"target=%%~t\"\r\necho patched > \"%target%\"\r\nexit /b 0\r\n");
}
else
{
Expand Down
32 changes: 15 additions & 17 deletions Optimum.Bootstrap.Core.Tests/IlspycmdVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,25 @@ public class IlspycmdVersionTests
private static readonly IlspycmdCompatibility Range = IlspycmdCompatibility.Fallback;

[Theory]
[InlineData("10.1.0.8386")]
[InlineData("10.1.0.8387")]
[InlineData("10.1.1.0")]
[InlineData("10.1.1.8387")]
[InlineData("10.1.1.8388")]
[InlineData("11.0.0.9375")]
public void AcceptsVersionsInsideTheRange(string version)
{
Assert.True(Range.Supports(version));
}

[Theory]
[InlineData("10.1.0.8385")]
[InlineData("10.1.1.8389")]
[InlineData("10.1.2.9000")]
[InlineData("10.0.1.8346")]
[InlineData("11.0.0.9374")]
[InlineData("11.0.0.9376")]
[InlineData("10.1.1.8388")]
[InlineData("10.1.0.8386")]
[InlineData("11.0.1.0")]
[InlineData("11.1.0.0")]
[InlineData("12.0.0.0")]
[InlineData("10.2.0.1")]
[InlineData("10.0.0.8323-preview3")]
[InlineData("10.1.1.8388-rc1")]
[InlineData("11.0.0.9375-rc1")]
[InlineData("")]
[InlineData("not-a-version")]
[InlineData("10.1.1")]
[InlineData("11.0.0")]
public void RejectsEverythingElse(string version)
{
Assert.False(Range.Supports(version));
Expand All @@ -44,15 +42,15 @@ public void ReadsTheRangeAndPinFromConfigFiles()
{
var probe = new FakeSystemProbe();
probe.AddFile("/repo/.config/ilspycmd-compat.json",
"""{ "minimumVersion": "10.1.0.8386", "maximumVersion": "10.1.1.8388" }""");
"""{ "minimumVersion": "11.0.0.9375", "maximumVersion": "11.0.0.9375" }""");
probe.AddFile("/repo/.config/dotnet-tools.json",
"""{ "version": 1, "tools": { "ilspycmd": { "version": "10.1.1.8388" } } }""");
"""{ "version": 1, "tools": { "ilspycmd": { "version": "11.0.0.9375" } } }""");

IlspycmdCompatibility compat = ConfigFiles.ReadIlspycmdCompatibility(probe, "/repo");

Assert.Equal("10.1.1.8388", compat.Pin);
Assert.Equal(new IlspycmdVersion(10, 1, 0, 8386), compat.Minimum);
Assert.Equal(new IlspycmdVersion(10, 1, 1, 8388), compat.Maximum);
Assert.Equal("11.0.0.9375", compat.Pin);
Assert.Equal(new IlspycmdVersion(11, 0, 0, 9375), compat.Minimum);
Assert.Equal(new IlspycmdVersion(11, 0, 0, 9375), compat.Maximum);
}

[Fact]
Expand Down
6 changes: 3 additions & 3 deletions Optimum.Bootstrap.Core.Tests/PrerequisiteScannerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ public void AllRequiredPresentWhenTheSdkAndAnInRangeDecompilerAreThere()
probe.OnCommand("/home/tester/.dotnet/dotnet", "--list-sdks", "10.0.100 [/user/sdk]\n");
probe.OnCommand("/home/tester/.dotnet/dotnet", "--version", "10.0.100\n");
probe.AddFile("/home/tester/.dotnet/tools/ilspycmd");
probe.OnCommand("/home/tester/.dotnet/tools/ilspycmd", "--version", "ilspycmd: 10.1.1.8388\n");
probe.OnCommand("/home/tester/.dotnet/tools/ilspycmd", "--version", "ilspycmd: 11.0.0.9375\n");

var scanner = new PrerequisiteScanner(probe, "/repo");
Assert.True(scanner.AllRequiredPresent());

PrerequisiteResult ilspy = scanner.Scan().Single(r => r.Definition.Id == PrerequisiteId.Ilspycmd);
Assert.Equal(PrerequisiteState.Ok, ilspy.State);
Assert.Equal("10.1.1.8388", ilspy.DetectedVersion);
Assert.Equal("11.0.0.9375", ilspy.DetectedVersion);
}

[Fact]
Expand All @@ -93,7 +93,7 @@ public void AnOutOfRangeDecompilerIsReportedOutdatedWithTheUpdateCommand()

Assert.Equal(PrerequisiteState.Outdated, ilspy.State);
Assert.Equal(
"dotnet tool update -g ilspycmd --version 10.1.1.8388 --allow-downgrade",
"dotnet tool update -g ilspycmd --version 11.0.0.9375 --allow-downgrade",
ilspy.AcquisitionCommand);
}

Expand Down
17 changes: 11 additions & 6 deletions Optimum.Bootstrap.Core.Tests/ShortcutWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,29 @@ public sealed class ShortcutWriterTests : IDisposable

private FakeSystemProbe LinuxProbe()
{
var probe = new FakeSystemProbe { Os = OsKind.Linux, HomeDirectory = _home };
probe.Environment["XDG_DATA_HOME"] = Path.Combine(_home, ".local", "share");
var probe = new FakeSystemProbe { Os = OsKind.Linux, HomeDirectory = _home.Replace('\\', '/') };
probe.Environment["XDG_DATA_HOME"] = _home.Replace('\\', '/') + "/.local/share";
return probe;
}

[Fact]
public void WritesAndRemovesTheLinuxMenuAndDesktopEntries()
{
FakeSystemProbe probe = LinuxProbe();
string installDir = Path.Combine(_home, "games", "optimum");
string launcher = Path.Combine(installDir, "optimum-launch.sh");
// A Linux install uses '/' paths. Build them with '/' so the generated
// .desktop Exec matches regardless of the host running the test (on
// Windows Path.Combine would inject '\', which the Desktop Entry writer
// then backslash-escapes, diverging from this assertion).
string home = _home.Replace('\\', '/');
string installDir = $"{home}/games/optimum";
string launcher = $"{installDir}/optimum-launch.sh";
Directory.CreateDirectory(installDir);

var writer = new ShortcutWriter(probe);
IReadOnlyList<string> created = writer.Create(installDir, launcher, ShortcutKinds.Menu | ShortcutKinds.Desktop);

string menuEntry = Path.Combine(_home, ".local", "share", "applications", "optimum.desktop");
string desktopEntry = Path.Combine(_home, "Desktop", "Optimum.desktop");
string menuEntry = $"{home}/.local/share/applications/optimum.desktop";
string desktopEntry = $"{home}/Desktop/Optimum.desktop";
Assert.Contains(menuEntry, created);
Assert.Contains(desktopEntry, created);
Assert.True(File.Exists(menuEntry));
Expand Down
10 changes: 5 additions & 5 deletions Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@ public async Task DoesNotReuseAnIncompleteWindowsCheckout()
public async Task ReusesACompleteWindowsCheckoutFromTheSourceCache()
{
var probe = new FakeSystemProbe { Os = OsKind.Windows };
probe.Environment["LOCALAPPDATA"] = "/cache";
string cached = "/cache/optimum/src-v0.3.14";
probe.AddFile($"{cached}/forks.json");
probe.AddFile($"{cached}/scripts/bootstrap.ps1");
probe.AddFile($"{cached}/scripts/package.ps1");
probe.Environment["LOCALAPPDATA"] = @"C:\cache";
string cached = @"C:\cache\optimum\src-v0.3.14";
probe.AddFile($@"{cached}\forks.json");
probe.AddFile($@"{cached}\scripts\bootstrap.ps1");
probe.AddFile($@"{cached}\scripts\package.ps1");
// No Git on PATH: proves the complete cached source is the one used.

var result = await new GitSourceProvider(probe)
Expand Down
5 changes: 3 additions & 2 deletions Optimum.Bootstrap.Core/Acquisition/SdkAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ public static Decision Evaluate(ISystemProbe probe, string repoRoot)
"This is a non-FHS system: the SDK from dot.net is a glibc build whose dynamic linker is not present here.", null);
}

string installDir = Path.Combine(probe.HomeDirectory, ".dotnet");
string globalJson = Path.Combine(repoRoot, "global.json");
bool windows = probe.Os == OsKind.Windows;
char sep = windows ? '\\' : '/';
string installDir = probe.HomeDirectory.TrimEnd('/', '\\') + sep + ".dotnet";
string globalJson = repoRoot.TrimEnd('/', '\\') + sep + "global.json";

var args = windows
? new List<string> { "-InstallDir", installDir, "-NoPath" }
Expand Down
24 changes: 20 additions & 4 deletions Optimum.Bootstrap.Core/Build/RepoRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,32 @@ public static class RepoRoot
{
public static string? Discover(ISystemProbe probe, string? explicitRoot = null)
{
string start = explicitRoot is not null
? Path.GetFullPath(explicitRoot)
: Directory.GetCurrentDirectory();
// An explicit root is already an absolute path in the probe's world, so
// walk it as given. Only the implicit case needs the real working
// directory. Resolving an explicit path through Path.GetFullPath would
// rewrite a POSIX path against the host drive on Windows, which is wrong
// whenever the probe models a different platform (and in tests).
string start = explicitRoot ?? Directory.GetCurrentDirectory();

for (string? dir = start; dir is not null; dir = Path.GetDirectoryName(dir))
for (string? dir = start; dir is not null; dir = ParentOf(dir))
{
if (SourceCache.IsUsableCheckout(probe, dir))
return dir;
}

return null;
}

/// <summary>
/// The parent of a path, honouring both separators so a POSIX path resolves
/// the same way regardless of the host the binary runs on. Returns null at
/// the root so the walk terminates.
/// </summary>
private static string? ParentOf(string dir)
{
int cut = dir.TrimEnd('/', '\\').LastIndexOfAny(['/', '\\']);
if (cut < 0)
return null;
return cut == 0 ? dir[..1] : dir[..cut];
}
}
Loading