ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
5 changed files with 145336 additions and 0 deletions
5
packages/fsharp-runner/hostexec/AssemblyInfo.cs
Normal file
5
packages/fsharp-runner/hostexec/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
[assembly: InternalsVisibleTo("HostExec.Tests")]
|
||||
[assembly: SupportedOSPlatform("linux")]
|
||||
114
packages/fsharp-runner/hostexec/GnuProxy.cs
Normal file
114
packages/fsharp-runner/hostexec/GnuProxy.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Quorate.HostExec;
|
||||
|
||||
[SuppressMessage("Maintainability", "CA1515", Justification = "Loaded by F# actions.")]
|
||||
public enum GnuTool
|
||||
{
|
||||
C,
|
||||
Cpp,
|
||||
Gcc,
|
||||
Gxx
|
||||
}
|
||||
|
||||
[SuppressMessage("Maintainability", "CA1515", Justification = "Loaded by F# actions.")]
|
||||
public sealed class ToolRoot
|
||||
{
|
||||
private ToolRoot(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public static ToolRoot Open(string path)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||
|
||||
if (!System.IO.Path.IsPathFullyQualified(path))
|
||||
{
|
||||
throw new ArgumentException("tool root must be absolute", nameof(path));
|
||||
}
|
||||
|
||||
var fullPath = System.IO.Path.GetFullPath(path);
|
||||
|
||||
if (!Directory.Exists(fullPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException(fullPath);
|
||||
}
|
||||
|
||||
return new ToolRoot(fullPath);
|
||||
}
|
||||
|
||||
internal string Resolve(string relative)
|
||||
{
|
||||
if (System.IO.Path.IsPathFullyQualified(relative))
|
||||
{
|
||||
throw new ArgumentException("tool path must be relative", nameof(relative));
|
||||
}
|
||||
|
||||
var path = System.IO.Path.GetFullPath(System.IO.Path.Combine(Path, relative));
|
||||
var prefix = Path + System.IO.Path.DirectorySeparatorChar;
|
||||
|
||||
if (!path.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("tool path escapes its root", nameof(relative));
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Maintainability", "CA1515", Justification = "Loaded by F# actions.")]
|
||||
public static class GnuProxy
|
||||
{
|
||||
private const string Launcher = "#!/usr/share/dotnet/dotnet /opt/fsi/packages/HostExec.dll\n";
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public static string Install(ToolRoot root, GnuTool tool)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(root);
|
||||
var name = Name(tool);
|
||||
var relative = $"usr/bin/{name}";
|
||||
var executable = root.Resolve(relative);
|
||||
|
||||
if (!File.Exists(executable))
|
||||
{
|
||||
throw new FileNotFoundException("GNU tool is unavailable", executable);
|
||||
}
|
||||
|
||||
var target = root.Resolve($"usr/local/bin/{name}");
|
||||
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(target)!);
|
||||
File.WriteAllText(target, Launcher);
|
||||
File.SetUnixFileMode(
|
||||
target,
|
||||
UnixFileMode.UserRead
|
||||
| UnixFileMode.UserWrite
|
||||
| UnixFileMode.UserExecute
|
||||
| UnixFileMode.GroupRead
|
||||
| UnixFileMode.GroupExecute
|
||||
| UnixFileMode.OtherRead
|
||||
| UnixFileMode.OtherExecute
|
||||
);
|
||||
|
||||
File.WriteAllText($"{target}.json", JsonSerializer.Serialize(new ProxyFile(1, "gnu", relative), Json));
|
||||
return target;
|
||||
}
|
||||
|
||||
private static string Name(GnuTool tool) =>
|
||||
tool switch
|
||||
{
|
||||
GnuTool.C => "cc",
|
||||
GnuTool.Cpp => "c++",
|
||||
GnuTool.Gcc => "gcc",
|
||||
GnuTool.Gxx => "g++",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(tool))
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed record ProxyFile(int Version, string Kind, string File);
|
||||
9
packages/fsharp-runner/hostexec/HostExec.csproj
Normal file
9
packages/fsharp-runner/hostexec/HostExec.csproj
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseAppHost>false</UseAppHost>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
25
packages/fsharp-runner/hostexec/Program.cs
Normal file
25
packages/fsharp-runner/hostexec/Program.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Quorate.HostExec;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[SuppressMessage("Design", "CA1031", Justification = "The executable boundary maps failures to the proxy exit code.")]
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
await Console.Error.WriteLineAsync("proxy path is missing").ConfigureAwait(false);
|
||||
return 125;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await Proxy.Run(args[0], args[1..]).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"{error.GetType().Name}: {error.Message}").ConfigureAwait(false);
|
||||
return 125;
|
||||
}
|
||||
}
|
||||
}
|
||||
119
packages/fsharp-runner/hostexec/Proxy.cs
Normal file
119
packages/fsharp-runner/hostexec/Proxy.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Quorate.HostExec;
|
||||
|
||||
internal static class Proxy
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public static async Task<int> Run(string wrapper, IReadOnlyList<string> arguments)
|
||||
{
|
||||
var fullWrapper = Path.GetFullPath(wrapper);
|
||||
var root = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(fullWrapper)!, "../../.."));
|
||||
var config = Read($"{fullWrapper}.json");
|
||||
|
||||
if (config.Version != 1 || config.Kind != "gnu")
|
||||
{
|
||||
throw new InvalidDataException("unsupported proxy configuration");
|
||||
}
|
||||
|
||||
var toolRoot = ToolRoot.Open(root);
|
||||
var executable = toolRoot.Resolve(config.File);
|
||||
|
||||
if (!File.Exists(executable))
|
||||
{
|
||||
throw new FileNotFoundException("proxied tool is unavailable", executable);
|
||||
}
|
||||
|
||||
var start = new ProcessStartInfo(executable)
|
||||
{
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
foreach (var argument in WithSysroot(root, arguments))
|
||||
{
|
||||
start.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
using var stop = new CancellationTokenSource();
|
||||
var signalCode = 143;
|
||||
using var interrupt = PosixSignalRegistration.Create(PosixSignal.SIGINT, context =>
|
||||
{
|
||||
context.Cancel = true;
|
||||
signalCode = 130;
|
||||
stop.Cancel();
|
||||
});
|
||||
using var terminate = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
|
||||
{
|
||||
context.Cancel = true;
|
||||
signalCode = 143;
|
||||
stop.Cancel();
|
||||
});
|
||||
using var child = Process.Start(start) ?? throw new InvalidOperationException("proxied tool did not start");
|
||||
|
||||
try
|
||||
{
|
||||
await child.WaitForExitAsync(stop.Token).ConfigureAwait(false);
|
||||
return child.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException) when (stop.IsCancellationRequested)
|
||||
{
|
||||
if (!child.HasExited)
|
||||
{
|
||||
child.Kill(true);
|
||||
await child.WaitForExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return signalCode;
|
||||
}
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<string> WithSysroot(string root, IReadOnlyList<string> arguments)
|
||||
{
|
||||
var fullRoot = Path.GetFullPath(root);
|
||||
var found = false;
|
||||
|
||||
for (var index = 0; index < arguments.Count; index++)
|
||||
{
|
||||
var argument = arguments[index];
|
||||
string? value = null;
|
||||
|
||||
if (argument == "--sysroot")
|
||||
{
|
||||
if (++index >= arguments.Count)
|
||||
{
|
||||
throw new ArgumentException("sysroot value is missing", nameof(arguments));
|
||||
}
|
||||
|
||||
value = arguments[index];
|
||||
}
|
||||
else if (argument.StartsWith("--sysroot=", StringComparison.Ordinal))
|
||||
{
|
||||
value = argument[10..];
|
||||
}
|
||||
|
||||
if (value is not null)
|
||||
{
|
||||
if (!Path.GetFullPath(value).Equals(fullRoot, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("conflicting sysroot", nameof(arguments));
|
||||
}
|
||||
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
return found ? arguments : [$"--sysroot={fullRoot}", .. arguments];
|
||||
}
|
||||
|
||||
private static ProxyFile Read(string path)
|
||||
{
|
||||
var value = JsonSerializer.Deserialize<ProxyFile>(File.ReadAllBytes(path), Json);
|
||||
return value ?? throw new InvalidDataException("proxy configuration is empty");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue