Skip to content

Commit

Permalink
Adding IncludePath metadata for complex include hierarchies (#268)
Browse files Browse the repository at this point in the history
* Added support for flatc -I argument

* Removing unnecessary using

* Fixed typo

* Added compiler tests for FBS include statement with/without IncludePath
  • Loading branch information
yak-shaver committed Jan 22, 2022
1 parent f60e37a commit 327e990
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 27 deletions.
3 changes: 3 additions & 0 deletions src/FlatSharp.Compiler/CompilerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public record CompilerOptions
[Option('o', "output", HelpText = "Output directory", Required = true)]
public string? OutputDirectory { get; set; }

[Option('I', "includes", HelpText = "Includes search directory path(s)")]
public string? IncludesDirectory { get; set; }

[Option("nullable-warnings", Default = false)]
public bool? NullableWarnings { get; set; }

Expand Down
74 changes: 71 additions & 3 deletions src/FlatSharp.Compiler/FlatSharp.Compiler.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project>

<!-- Task to process the FlatSharpSchema:
- Filters out duplicate files.
- Transforms the 'IncludePath' metadata property to absolute paths (semi-colon separated). -->
<UsingTask TaskName="ProcessFlatSharpSchema" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Inputs ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.IO" />
<Code Type="Fragment" Language="cs">
<![CDATA[
if (Inputs == null || Inputs.Length == 0)
{
Result = Array.Empty<ITaskItem>();
return true;
}
var errors = new HashSet<string>();
var alreadySeen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var outputList = new List<ITaskItem>(Inputs.Length);
foreach (var item in Inputs)
{
if (alreadySeen.Add(item.ItemSpec))
{
var includePath = String.Join(";", item.GetMetadata("IncludePath").Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(include =>
{
if (!Directory.Exists(include))
{
errors.Add(string.Format("FlatSharpSchema.IncludePath must only contain existing directories, separated by a semi-colon. '{0}' is not a valid directory.", include));
return null;
}
return Path.GetFullPath(include);
}));
item.SetMetadata("IncludePath", includePath);
outputList.Add(item);
}
}
if (errors.Any())
{
foreach (var error in errors)
{
Log.LogError(error);
}
Result = Array.Empty<ITaskItem>();
return false;
}
Result = outputList.ToArray();
return true;
]]>
</Code>
</Task>
</UsingTask>

<!-- Tell Visual Studio that fbs files can result in build changes.
https://github.com/dotnet/project-system/blob/cd275918ef9f181f6efab96715a91db7aabec832/docs/up-to-date-check.md -->
<ItemGroup>
Expand Down Expand Up @@ -37,11 +97,19 @@
Condition=" '$(CompilerPath)' == '' "
Text="FlatSharp.Compiler requires .NET Core 3.1, .NET 5.0, or .NET 6.0 to be installed and available on the PATH." />

<Message Text="dotnet $(CompilerPath) --nullable-warnings $(FlatSharpNullable) --input &quot;%(FlatSharpSchema.fullpath)&quot; --output $(IntermediateOutputPath)" Importance="high" />
<ProcessFlatSharpSchema Inputs="@(FlatSharpSchema)">
<Output TaskParameter="Result" ItemName="ProcessedFlatSharpSchema" />
</ProcessFlatSharpSchema>

<Message
Text="dotnet $(CompilerPath) --nullable-warnings $(FlatSharpNullable) --input &quot;%(ProcessedFlatSharpSchema.fullpath)&quot; --includes &quot;%(ProcessedFlatSharpSchema.IncludePath)&quot; --output $(IntermediateOutputPath)"
Condition=" '%(ProcessedFlatSharpSchema.fullpath)' != '' "
Importance="high" />

<Exec
Command="dotnet $(CompilerPath) --nullable-warnings $(FlatSharpNullable) --input &quot;%(FlatSharpSchema.fullpath)&quot; --output $(IntermediateOutputPath) "
Command="dotnet $(CompilerPath) --nullable-warnings $(FlatSharpNullable) --input &quot;%(ProcessedFlatSharpSchema.fullpath)&quot; --includes &quot;%(ProcessedFlatSharpSchema.IncludePath)&quot; --output $(IntermediateOutputPath)"
CustomErrorRegularExpression=".*"
Condition=" '%(FlatSharpSchema.fullpath)' != '' " />
Condition=" '%(ProcessedFlatSharpSchema.fullpath)' != '' " />

<ItemGroup>
<GeneratedFbs Include="$(IntermediateOutputPath)*.generated.cs" />
Expand Down
127 changes: 103 additions & 24 deletions src/FlatSharp.Compiler/FlatSharpCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,11 @@ internal static (Assembly, string) CompileAndLoadAssemblyWithCode(
string fbsFile = Path.GetTempFileName() + ".fbs";
try
{
Assembly[] additionalRefs = additionalReferences?.ToArray() ?? Array.Empty<Assembly>();

File.WriteAllText(fbsFile, fbsSchema);
options.InputFile = fbsFile;

byte[] bfbs = GetBfbs(options);
CreateCSharp(bfbs, "hash", options, out string cSharp);

var (assembly, formattedText, _) = RoslynSerializerGenerator.CompileAssembly(cSharp, true, additionalRefs);
string debugText = formattedText();
return (assembly, cSharp);
return CompileAndLoadAssemblyWithCode(new FileInfo(fbsFile), options, additionalReferences);
}
finally
{
ErrorContext.Current.Clear();
File.Delete(fbsFile);
}
}
Expand All @@ -216,7 +206,80 @@ internal static Assembly CompileAndLoadAssembly(
return asm;
}

internal static byte[] GetBfbs(CompilerOptions options)
// Test hook
internal static Assembly[] CompileAndLoadAssemblies(
IEnumerable<(string FileName, string Content)> fbsSchemas,
CompilerOptions options,
IEnumerable<Assembly>? additionalReferences = null)
{
Assembly[] additionalRefs = additionalReferences?.ToArray() ?? Array.Empty<Assembly>();
var assemblies = new List<Assembly>();

var tempDir = Path.GetFileNameWithoutExtension(Path.GetTempFileName());

if (!string.IsNullOrEmpty(options.IncludesDirectory))
{
// Convert includes directories into absolute paths as would be done by the targets file
var paths = options.IncludesDirectory.Split(';', StringSplitOptions.RemoveEmptyEntries);
options.IncludesDirectory = string.Join(';', paths.Select(path => Path.GetFullPath(Path.Combine(tempDir, path))));
}

try
{
Directory.CreateDirectory(tempDir);

var fbsFiles = new List<FileInfo>();

foreach (var fbsSchema in fbsSchemas)
{
var fbsFile = new FileInfo(Path.Combine(tempDir, fbsSchema.FileName));
fbsFile.Directory?.Create();
File.WriteAllText(fbsFile.FullName, fbsSchema.Content);

fbsFiles.Add(fbsFile);
}

foreach (var fbsFile in fbsFiles)
{
var (asm, _) = CompileAndLoadAssemblyWithCode(fbsFile, options, additionalRefs);
assemblies.Add(asm);
additionalRefs = additionalRefs.Append(asm).ToArray();
}
}
finally
{
Directory.Delete(tempDir, true);
}

return assemblies.ToArray();
}

// Test hook
private static (Assembly, string) CompileAndLoadAssemblyWithCode(
FileInfo fbsFile,
CompilerOptions options,
IEnumerable<Assembly>? additionalReferences = null)
{
try
{
Assembly[] additionalRefs = additionalReferences?.ToArray() ?? Array.Empty<Assembly>();

options.InputFile = fbsFile.FullName;

byte[] bfbs = GetBfbs(options);
CreateCSharp(bfbs, "hash", options, out string cSharp);

var (assembly, formattedText, _) = RoslynSerializerGenerator.CompileAssembly(cSharp, true, additionalRefs);
string debugText = formattedText();
return (assembly, cSharp);
}
finally
{
ErrorContext.Current.Clear();
}
}

private static byte[] GetBfbs(CompilerOptions options)
{
string flatcPath;

Expand Down Expand Up @@ -273,19 +336,35 @@ internal static byte[] GetBfbs(CompilerOptions options)
}
};

foreach (var arg in new[]
var args = new List<string>
{
"-b",
"--schema",
"--bfbs-comments",
"--bfbs-builtins",
"--bfbs-filenames",
info.DirectoryName!, // Files always have a directory name, dammit!
"--no-warnings",
"-o",
outputDir,
info.FullName
})
"-b",
"--schema",
"--bfbs-comments",
"--bfbs-builtins",
"--bfbs-filenames",
info.DirectoryName!, // Files always have a directory name, dammit!
"--no-warnings",
"-o",
outputDir,
};

if (!string.IsNullOrEmpty(options.IncludesDirectory))
{
// One or more includes directory has been specified
foreach (var includePath in options.IncludesDirectory.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
args.AddRange(new[]
{
"-I",
new DirectoryInfo(includePath).FullName,
});
}
}

args.Add(info.FullName);

foreach (var arg in args)
{
p.StartInfo.ArgumentList.Add(arg);
}
Expand Down
143 changes: 143 additions & 0 deletions src/Tests/FlatSharpCompilerTests/IncludeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2021 James Courtney
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace FlatSharpTests.Compiler;

public class IncludeTests
{
[Fact]
public void IncludeTest()
{
var schemaA = $@"
include ""B.fbs"";
namespace Foobar;
table A {{ TableB:B; }}
";

var schemaB = $@"
namespace Foobar;
table B {{ Value:int32; }}
";

var schemas = new[]
{
("B.fbs", schemaB),
("A.fbs", schemaA),
};

var assemblies = FlatSharpCompiler.CompileAndLoadAssemblies(schemas, new());

UsingAssemblies(assemblies, () =>
{
Type aType = assemblies[1].GetType("Foobar.A");
Type bType = assemblies[0].GetType("Foobar.B");
PropertyInfo tableB = aType.GetProperty("TableB");
Assert.Equal(bType, tableB.PropertyType);
var compiled = CompilerTestHelpers.CompilerTestSerializer.Compile(aType);
byte[] data = new byte[100];
compiled.Write(data, Activator.CreateInstance(aType));
compiled.Parse(data);
});
}

[Fact]
public void IncludeTest_IncludePaths()
{
var schemaA = $@"
include ""Foo\\B.fbs"";
namespace Foobar;
table A {{ TableB:B; }}
";

var schemaB = $@"
include ""C.fbs"";
include ""D.fbs"";
namespace Foobar;
table B {{ TableC:C; TableD:D; }}
";

var schemaC = $@"
namespace Foobar;
table C {{ Value:int32; }}
";

var schemaD = $@"
namespace Foobar;
table D {{ Value:int32; }}
";

var schemas = new[]
{
(@"Baz\D.fbs", schemaD),
(@"Bar\C.fbs", schemaC),
(@"Foo\B.fbs", schemaB),
(@"A.fbs", schemaA),
};

var assemblies = FlatSharpCompiler.CompileAndLoadAssemblies(schemas, new() { IncludesDirectory = "Bar;Baz" });

UsingAssemblies(assemblies, () =>
{
Type aType = assemblies[3].GetType("Foobar.A");
Type bType = assemblies[2].GetType("Foobar.B");
Type cType = assemblies[1].GetType("Foobar.C");
Type dType = assemblies[0].GetType("Foobar.D");
PropertyInfo tableB = aType.GetProperty("TableB");
PropertyInfo tableC = bType.GetProperty("TableC");
PropertyInfo tableD = bType.GetProperty("TableD");
Assert.Equal(bType, tableB.PropertyType);
Assert.Equal(cType, tableC.PropertyType);
Assert.Equal(dType, tableD.PropertyType);
var compiled = CompilerTestHelpers.CompilerTestSerializer.Compile(aType);
byte[] data = new byte[100];
compiled.Write(data, Activator.CreateInstance(aType));
compiled.Parse(data);
});
}

private void UsingAssemblies(IEnumerable<Assembly> assemblies, Action action)
{
ResolveEventHandler handler = (s, e) =>
assemblies.FirstOrDefault(asm => asm.GetName().Name == new AssemblyName(e.Name).Name);

try
{
AppDomain.CurrentDomain.AssemblyResolve += handler;
action.Invoke();
}
finally
{
AppDomain.CurrentDomain.AssemblyResolve -= handler;
}
}
}

0 comments on commit 327e990

Please sign in to comment.