First release (#10)
Some checks are pending
.NET / build (Debug) (push) Waiting to run
.NET / build (Release) (push) Waiting to run
.NET / analyzers (push) Waiting to run
.NET / check-dotnet-format (push) Waiting to run
.NET / check-nix-format (push) Waiting to run
.NET / Check links (push) Waiting to run
.NET / Check flake (push) Waiting to run
.NET / nuget-pack (push) Waiting to run
.NET / expected-pack (push) Blocked by required conditions
.NET / check-accurate-generations (push) Waiting to run
.NET / all-required-checks-complete (push) Blocked by required conditions
.NET / nuget-publish (push) Blocked by required conditions
.NET / nuget-publish-fantomas (push) Blocked by required conditions
.NET / nuget-publish-json-plugin (push) Blocked by required conditions
.NET / nuget-publish-json-attrs (push) Blocked by required conditions
.NET / nuget-publish-argparser-plugin (push) Blocked by required conditions
.NET / nuget-publish-argparser-attrs (push) Blocked by required conditions

This commit is contained in:
Patrick Stevens
2024-10-07 13:35:43 +01:00
committed by GitHub
parent dc7a0f6fc2
commit da609db2ce
60 changed files with 14225 additions and 67 deletions

View File

@@ -0,0 +1,33 @@
namespace WoofWare.Whippet
open System
open System.IO
open System.Reflection
// Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8
// (This tells the DLL loader to look next to the input DLL for dependencies.)
/// Context manager to set the AppContext.BaseDirectory of the executing DLL.
type SetBaseDir (testDll : FileInfo) =
let oldBaseDir = AppContext.BaseDirectory
let setData =
let appContext = Type.GetType "System.AppContext"
if Object.ReferenceEquals (appContext, (null : obj)) then
ignore<string * string>
else
let setDataMethod =
appContext.GetMethod ("SetData", BindingFlags.Static ||| BindingFlags.Public)
if Object.ReferenceEquals (setDataMethod, (null : obj)) then
ignore<string * string>
else
fun (k, v) -> setDataMethod.Invoke ((null : obj), [| k ; v |]) |> unbox<unit>
do setData ("APP_CONTEXT_BASE_DIRECTORY", testDll.Directory.FullName)
interface IDisposable with
member _.Dispose () =
setData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir)

View File

@@ -0,0 +1,26 @@
namespace WoofWare.Whippet
open System.IO
open System.Reflection
open System.Runtime.Loader
type Ctx (dll : FileInfo, runtimes : DirectoryInfo list) =
inherit AssemblyLoadContext ()
override this.Load (target : AssemblyName) : Assembly =
let path = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll")
if File.Exists path then
this.LoadFromAssemblyPath path
else
runtimes
|> List.tryPick (fun di ->
let path = Path.Combine (di.FullName, $"%s{target.Name}.dll")
if File.Exists path then
this.LoadFromAssemblyPath path |> Some
else
None
)
|> Option.defaultValue null

View File

@@ -0,0 +1,219 @@
namespace WoofWare.Whippet
open System
open System.IO
open System.Reflection
open Ionide.ProjInfo
open Ionide.ProjInfo.Types
open WoofWare.Whippet.Core
type Args =
{
InputFile : FileInfo
Plugins : FileInfo list
}
type WhippetTarget =
{
InputSource : FileInfo
GeneratedDest : FileInfo
Params : Map<string, string>
}
module Program =
let parseArgs (argv : string array) =
let inputFile = argv.[0] |> FileInfo
{
InputFile = inputFile
Plugins = argv.[1..] |> Seq.map FileInfo |> Seq.toList
}
let getGenerateRawFromRaw (host : obj) : (RawSourceGenerationArgs -> string option) option =
let pluginType = host.GetType ()
let generateRawFromRaw =
match
pluginType.GetMethod (
"GenerateRawFromRaw",
BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.FlattenHierarchy
)
|> Option.ofObj
with
| None ->
pluginType.GetInterfaces ()
|> Array.tryPick (fun interf ->
interf.GetMethod (
"GenerateRawFromRaw",
BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.FlattenHierarchy
)
|> Option.ofObj
)
| Some generateRawFromRaw -> Some generateRawFromRaw
match generateRawFromRaw with
| None -> None
| Some generateRawFromRaw ->
let pars = generateRawFromRaw.GetParameters ()
if pars.Length <> 1 then
failwith
$"Expected GenerateRawFromRaw to take exactly one parameter: a RawSourceGenerationArgs. Got %i{pars.Length}"
if pars.[0].ParameterType.FullName <> typeof<RawSourceGenerationArgs>.FullName then
failwith
$"Expected GenerateRawFromRaw to take exactly one parameter: a RawSourceGenerationArgs. Got %s{pars.[0].ParameterType.FullName}"
let retType = generateRawFromRaw.ReturnType
if retType <> typeof<string> then
failwith
$"Expected GenerateRawFromRaw method to have return type `string`, but was: %s{retType.FullName}"
fun args ->
let args =
Activator.CreateInstance (
pars.[0].ParameterType,
[| box args.FilePath ; box args.FileContents ; box args.Parameters |]
)
generateRawFromRaw.Invoke (host, [| args |]) |> unbox<string> |> Option.ofObj
|> Some
[<EntryPoint>]
let main argv =
let args = parseArgs argv
let projectDirectory = args.InputFile.Directory
let toolsPath = Init.init projectDirectory None
let defaultLoader = WorkspaceLoader.Create (toolsPath, [])
use subscription =
defaultLoader.Notifications.Subscribe (fun msg ->
match msg with
| WorkspaceProjectState.Loading projectFilePath ->
Console.Error.WriteLine $"Loading: %s{projectFilePath}"
| WorkspaceProjectState.Loaded (loadedProject, _knownProjects, fromCache) ->
let fromCache = if fromCache then " (from cache)" else ""
Console.Error.WriteLine $"Loaded %s{loadedProject.ProjectFileName}%s{fromCache}"
| WorkspaceProjectState.Failed (projectFilePath, errors) ->
let errors = errors.ToString ()
failwith $"Failed to load project %s{projectFilePath}: %s{errors}"
)
let projectOptions =
defaultLoader.LoadProjects [ args.InputFile.FullName ] |> Seq.toArray
let desiredProject =
projectOptions
|> Array.find (fun po -> po.ProjectFileName = args.InputFile.FullName)
let toGenerate =
desiredProject.Items
|> List.choose (fun (ProjectItem.Compile (_name, fullPath, metadata)) ->
match metadata with
| None -> None
| Some metadata ->
match Map.tryFind "WhippetFile" metadata with
| None -> None
| Some myriadFile ->
let pars =
metadata
|> Map.toSeq
|> Seq.choose (fun (key, value) ->
if key.StartsWith ("WhippetParam", StringComparison.Ordinal) then
Some (key.Substring "WhippetParam".Length, value)
else
None
)
|> Map.ofSeq
let inputSource =
FileInfo (Path.Combine (Path.GetDirectoryName desiredProject.ProjectFileName, myriadFile))
let generatedDest = FileInfo fullPath
if inputSource.FullName = generatedDest.FullName then
failwith $"Input source %s{inputSource.FullName} was identical to output path; aborting."
{
GeneratedDest = generatedDest
InputSource = inputSource
Params = pars
}
|> Some
)
let runtime =
DotnetRuntime.locate (Assembly.GetExecutingAssembly().Location |> FileInfo)
let pluginDll =
match args.Plugins with
| [] -> failwith "must supply a plugin!"
| [ plugin ] -> plugin
| _ -> failwith "We don't yet support running more than one Whippet plugin in a given project file"
// TODO: should ideally loop over files, not plugins, so we fully generate a file before moving on to the next
// one
Console.Error.WriteLine $"Loading plugin: %s{pluginDll.FullName}"
let ctx = Ctx (pluginDll, runtime)
let pluginAssembly = ctx.LoadFromAssemblyPath pluginDll.FullName
// We will look up any member called GenerateRawFromRaw and/or GenerateFromRaw.
// It's your responsibility to decide whether to do anything with this call; you return null if you don't want
// to do anything.
// Alternatively, return the text you want to output.
// We provide you with the input file contents.
// GenerateRawFromRaw should return plain text.
// GenerateFromRaw should return a Fantomas AST.
let applicablePlugins =
pluginAssembly.ExportedTypes
|> Seq.choose (fun ty ->
if
ty.CustomAttributes
|> Seq.exists (fun attr -> attr.AttributeType.Name = typeof<WhippetGeneratorAttribute>.Name)
then
Some (ty, Activator.CreateInstance ty)
else
None
)
|> Seq.toList
for item in toGenerate do
use output = item.GeneratedDest.Open (FileMode.Create, FileAccess.Write)
use outputWriter = new StreamWriter (output, leaveOpen = true)
for plugin, hostClass in applicablePlugins do
match getGenerateRawFromRaw hostClass with
| None -> ()
| Some generateRawFromRaw ->
let fileContents = File.ReadAllBytes item.InputSource.FullName
let args =
{
RawSourceGenerationArgs.FilePath = item.InputSource.FullName
FileContents = fileContents
Parameters = item.Params
}
let result = generateRawFromRaw args
match result with
| None
| Some null -> ()
| Some result ->
Console.Error.WriteLine
$"Writing output for generator %s{plugin.Name} to file %s{item.GeneratedDest.FullName}"
outputWriter.Write result
outputWriter.Write "\n"
()
0

View File

@@ -0,0 +1,47 @@
namespace WoofWare.Whippet
open System
type FrameworkDescription =
{
Name : string
Version : string
}
type RuntimeOptions =
{
Tfm : string
Framework : FrameworkDescription option
Frameworks : FrameworkDescription list option
RollForward : string option
}
type RuntimeConfig =
{
RuntimeOptions : RuntimeOptions
}
[<RequireQualifiedAccess>]
type RollForward =
| Minor
| Major
| LatestPatch
| LatestMinor
| LatestMajor
| Disable
static member Parse (s : string) : RollForward =
if s.Equals ("minor", StringComparison.OrdinalIgnoreCase) then
RollForward.Minor
elif s.Equals ("major", StringComparison.OrdinalIgnoreCase) then
RollForward.Major
elif s.Equals ("latestpatch", StringComparison.OrdinalIgnoreCase) then
RollForward.LatestPatch
elif s.Equals ("latestminor", StringComparison.OrdinalIgnoreCase) then
RollForward.LatestMinor
elif s.Equals ("latestmajor", StringComparison.OrdinalIgnoreCase) then
RollForward.LatestMajor
elif s.Equals ("disable", StringComparison.OrdinalIgnoreCase) then
RollForward.Disable
else
failwith $"Could not interpret '%s{s}' as a RollForward"

View File

@@ -0,0 +1,103 @@
namespace WoofWare.Whippet
(* File originally generated by Myriad. *)
/// Module containing JSON parsing methods for the FrameworkDescription type
[<RequireQualifiedAccess ; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module FrameworkDescription =
/// Parse from a JSON node.
let jsonParse (node : System.Text.Json.Nodes.JsonNode) : FrameworkDescription =
let arg_1 =
(match node.["version"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("version")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
let arg_0 =
(match node.["name"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("name")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
Name = arg_0
Version = arg_1
}
namespace WoofWare.Whippet
/// Module containing JSON parsing methods for the RuntimeOptions type
[<RequireQualifiedAccess ; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module RuntimeOptions =
/// Parse from a JSON node.
let jsonParse (node : System.Text.Json.Nodes.JsonNode) : RuntimeOptions =
let arg_3 =
match node.["rollForward"] with
| null -> None
| v -> v.AsValue().GetValue<System.String> () |> Some
let arg_2 =
match node.["frameworks"] with
| null -> None
| v ->
v.AsArray ()
|> Seq.map (fun elt -> FrameworkDescription.jsonParse elt)
|> List.ofSeq
|> Some
let arg_1 =
match node.["framework"] with
| null -> None
| v -> FrameworkDescription.jsonParse v |> Some
let arg_0 =
(match node.["tfm"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("tfm")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
Tfm = arg_0
Framework = arg_1
Frameworks = arg_2
RollForward = arg_3
}
namespace WoofWare.Whippet
/// Module containing JSON parsing methods for the RuntimeConfig type
[<RequireQualifiedAccess ; CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module RuntimeConfig =
/// Parse from a JSON node.
let jsonParse (node : System.Text.Json.Nodes.JsonNode) : RuntimeConfig =
let arg_0 =
RuntimeOptions.jsonParse (
match node.["runtimeOptions"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("runtimeOptions")
)
)
| v -> v
)
{
RuntimeOptions = arg_0
}

View File

@@ -0,0 +1,104 @@
namespace WoofWare.Whippet
open System
open System.IO
open WoofWare.DotnetRuntimeLocator
/// Functions for locating .NET runtimes.
[<RequireQualifiedAccess>]
module DotnetRuntime =
let private selectRuntime
(config : RuntimeOptions)
(f : DotnetEnvironmentInfo)
: Choice<DotnetEnvironmentFrameworkInfo, DotnetEnvironmentSdkInfo> option
=
let rollForward =
match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with
| null ->
config.RollForward
|> Option.map RollForward.Parse
|> Option.defaultValue RollForward.Minor
| s -> RollForward.Parse s
let desiredVersions =
match config.Framework with
| Some f -> [ Version f.Version, f.Name ]
| None ->
match config.Frameworks with
| Some f -> f |> List.map (fun f -> Version f.Version, f.Name)
| None ->
failwith
"Could not deduce a framework version due to lack of either Framework or Frameworks in runtimeconfig"
let compatiblyNamedRuntimes =
f.Frameworks
|> Seq.collect (fun availableFramework ->
desiredVersions
|> List.choose (fun (desiredVersion, desiredName) ->
if desiredName = availableFramework.Name then
Some
{|
Desired = desiredVersion
Name = desiredName
Installed = availableFramework
InstalledVersion = Version availableFramework.Version
|}
else
None
)
)
|> Seq.toList
match rollForward with
| RollForward.Minor ->
let available =
compatiblyNamedRuntimes
|> Seq.filter (fun data ->
data.InstalledVersion.Major = data.Desired.Major
&& data.InstalledVersion.Minor >= data.Desired.Minor
)
|> Seq.groupBy (fun data -> data.Name)
|> Seq.map (fun (name, data) ->
let data =
data
|> Seq.minBy (fun data -> data.InstalledVersion.Minor, data.InstalledVersion.Build)
name, data.Installed
)
// TODO: how do we select between many available frameworks?
|> Seq.tryHead
match available with
| Some (_, f) -> Some (Choice1Of2 f)
| None ->
// TODO: maybe we can ask the SDK. But we keep on trucking: maybe we're self-contained,
// and we'll actually find all the runtime next to the DLL.
None
| _ -> failwith "non-minor RollForward not supported yet; please shout if you want it"
/// Given an executable DLL, locate the .NET runtime that can best run it.
let locate (dll : FileInfo) : DirectoryInfo list =
let runtimeConfig =
let name =
if not (dll.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) then
failwith $"Expected DLL %s{dll.FullName} to end in .dll"
dll.Name.Substring (0, dll.Name.Length - 4)
Path.Combine (dll.Directory.FullName, $"%s{name}.runtimeconfig.json")
|> File.ReadAllText
|> System.Text.Json.Nodes.JsonNode.Parse
|> RuntimeConfig.jsonParse
|> fun f -> f.RuntimeOptions
let availableRuntimes = DotnetEnvironmentInfo.Get ()
let runtime = selectRuntime runtimeConfig availableRuntimes
match runtime with
| None ->
// Keep on trucking: let's be optimistic and hope that we're self-contained.
[ dll.Directory ]
| Some (Choice1Of2 runtime) -> [ dll.Directory ; DirectoryInfo $"%s{runtime.Path}/%s{runtime.Version}" ]
| Some (Choice2Of2 sdk) -> [ dll.Directory ; DirectoryInfo sdk.Path ]

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<Compile Include="RuntimeConfig.fs" />
<Compile Include="RuntimeConfigGen.fs" />
<Compile Include="AppContext.fs" />
<Compile Include="RuntimeLocator.fs" />
<Compile Include="Context.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WoofWare.DotnetRuntimeLocator" Version="0.1.9" />
<PackageReference Include="Ionide.ProjInfo" Version="0.67.0" PrivateAssets="compile" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.2.0" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="NuGet.Frameworks" Version="6.11.1" ExcludeAssets="runtime" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WoofWare.Whippet.Core\WoofWare.Whippet.Core.fsproj" />
</ItemGroup>
</Project>