diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..91343641 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "nuke.globaltool": { + "version": "8.0.0", + "commands": [ + "nuke" + ] + } + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..73e5a20a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +build.sh eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..505f01ea --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/perform-common-steps/action.yml b/.github/workflows/perform-common-steps/action.yml index a9ba89e9..11d9598c 100644 --- a/.github/workflows/perform-common-steps/action.yml +++ b/.github/workflows/perform-common-steps/action.yml @@ -4,16 +4,20 @@ runs: using: "composite" steps: - name: ⚙ Setup .NET SDK ⚙ - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.0.x' + global-json-file: global.json - name: ♻ NuGet Cache ♻ uses: actions/cache@v2 with: path: ${{ env.NUGET_PACKAGES }} - key: ${{ runner.os }}.nuget.${{ hashFiles('**/*.csproj') }} + key: ${{ runner.os }}.nuget.${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} + + - name: 🛠️ Restore local .NET tools 🛠️ + shell: bash + run: dotnet tool restore - name: 🔄 Restore Nuget Packages 🔄 shell: bash - run: dotnet restore + run: dotnet nuke RestoreAll diff --git a/.github/workflows/run-build-and-unit-tests.yml b/.github/workflows/run-build-and-unit-tests.yml index 840fbba7..68aeda4d 100644 --- a/.github/workflows/run-build-and-unit-tests.yml +++ b/.github/workflows/run-build-and-unit-tests.yml @@ -25,15 +25,23 @@ jobs: NUGET_PACKAGES: ${{ github.workspace }}/.github/nuget-packages steps: - name: 📝 Fetch Sources 📝 - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: 💡 Perform Common Steps 💡 uses: ./.github/workflows/perform-common-steps - name: 🛠 Build Solution 🛠 shell: bash - run: dotnet build + run: dotnet nuke CompileAll + + - name: 🚚 Publish Compiler Bundle 🚚 + shell: bash + run: dotnet nuke PublishCompilerBundle + + - name: 📦 Pack Compiler Bundle 📦 + shell: bash + run: dotnet nuke PackCompilerBundle - name: ✅ Run Unit Tests ✅ shell: bash - run: dotnet test + run: dotnet nuke TestAll diff --git a/.gitignore b/.gitignore index a58baf71..7794c572 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ bin/ obj/ +artifacts/ +/.nuke/temp *.dll *.exe *.runtimeconfig.json *.received.txt *.user + + diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 00000000..309bbb5e --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' or 'Release'", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "PublishAot": { + "type": "boolean", + "description": "If set to true, publishes compiler packs in AOT mode" + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "RuntimeId": { + "type": "string", + "description": "If set, only executes targets for a specified runtime identifier. Provided RID must be included in property of Cesium.Compiler project" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "CompileAll", + "ForceClear", + "PackAllCompilerBundles", + "PackCompilerBundle", + "PackSdk", + "PackTemplates", + "PublishAllCompilerBundles", + "PublishCompilerBundle", + "RestoreAll", + "TestAll", + "TestCodeGen", + "TestCompiler", + "TestIntegration", + "TestParser", + "TestRuntime", + "TestSdk" + ] + } + }, + "SkipCaches": { + "type": "boolean", + "description": "If set to true, ignores all cached build results. Default: false" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "CompileAll", + "ForceClear", + "PackAllCompilerBundles", + "PackCompilerBundle", + "PackSdk", + "PackTemplates", + "PublishAllCompilerBundles", + "PublishCompilerBundle", + "RestoreAll", + "TestAll", + "TestCodeGen", + "TestCompiler", + "TestIntegration", + "TestParser", + "TestRuntime", + "TestSdk" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 00000000..45c2a69b --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Cesium.sln" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb9d2cc9..448f2dfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,8 @@ There are two kinds of tests in Cesium: unit tests and integration tests. Run the unit and integration tests using this shell command: ```console -$ dotnet test +$ dotnet restore +$ dotnet nuke TestAll ``` Publishing diff --git a/Cesium.Ast/Cesium.Ast.csproj b/Cesium.Ast/Cesium.Ast.csproj index 444a7901..d2fdb902 100644 --- a/Cesium.Ast/Cesium.Ast.csproj +++ b/Cesium.Ast/Cesium.Ast.csproj @@ -1,8 +1,9 @@ - net7.0 + net8.0 enable + true diff --git a/Cesium.CodeGen.Tests/Cesium.CodeGen.Tests.csproj b/Cesium.CodeGen.Tests/Cesium.CodeGen.Tests.csproj index 1f0ee832..c47f9872 100644 --- a/Cesium.CodeGen.Tests/Cesium.CodeGen.Tests.csproj +++ b/Cesium.CodeGen.Tests/Cesium.CodeGen.Tests.csproj @@ -16,10 +16,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Cesium.CodeGen.Tests/CodeGenArrayTests.cs b/Cesium.CodeGen.Tests/CodeGenArrayTests.cs index f7f318e8..2d27961e 100644 --- a/Cesium.CodeGen.Tests/CodeGenArrayTests.cs +++ b/Cesium.CodeGen.Tests/CodeGenArrayTests.cs @@ -170,6 +170,24 @@ int main() { } """); + [Fact] + public Task SignedByteArrayTest() => DoTest(@" +int main() { + signed char x[1] = { -1 }; + signed char y = x[0]; + int z = (int)y; + return z; +}"); + + [Fact] + public Task UnSignedByteArrayTest2() => DoTest(@" +int main() { + unsigned char a[1] = { 255 }; + unsigned char b = a[0]; + int c = (int)b; + return c; +}"); + [Fact] public Task PointerAsArray() => DoTest(""" int main(void) diff --git a/Cesium.CodeGen.Tests/CodeGenMethodTests.cs b/Cesium.CodeGen.Tests/CodeGenMethodTests.cs index ae2a6075..0a12a018 100644 --- a/Cesium.CodeGen.Tests/CodeGenMethodTests.cs +++ b/Cesium.CodeGen.Tests/CodeGenMethodTests.cs @@ -77,6 +77,9 @@ int main() [Fact] public Task VoidParameterMain() => DoTest("int main(void){}"); [Fact] public Task PointerReceivingFunction() => DoTest("void foo(int *ptr){}"); [Fact] public Task StandardMain() => DoTest("int main(int argc, char *argv[]){}"); + [Fact] public Task PointerPointerMain() => DoTest("int main(int argc, char **argv){}"); + [Fact] public Task ConstConstMain() => DoTest("int main(int argc, const char * const *argv){}"); + [Fact] public Task ConstArgcMain() => DoTest("int main(const int argc, char* argv[]){}"); [Fact, NoVerify] public void NonstandardMainDoesNotCompile1() => DoesNotCompile("void main(){}", "Invalid return type"); [Fact, NoVerify] public void NonstandardMainDoesNotCompile2() => DoesNotCompile("int main(int c){}", "Invalid parameter"); [Fact, NoVerify] @@ -110,6 +113,16 @@ public Task Arithmetic() => DoTest(@"int main(void) } "); + [Fact] + public Task UnaryPlusAndStart() => DoTest(@" +int main() { + short a = -2; + short* b = &a; + short c = *b; + int x = (+c) - (+1) - (-1); + return sizeof(+c); +} "); + [Fact] public Task ReturnWithoutArgument() => DoTest(@"void console_read() { @@ -492,7 +505,7 @@ int console_read(struct struct1* s) { return s->x; }"); - // TODO [#196] + // TODO[#196] /* [Fact] public Task VarargFunctionPointerCallTest() => DoTest(@"int foo(int a, ...) { return a; } diff --git a/Cesium.CodeGen.Tests/CodeGenNetInteropTests.cs b/Cesium.CodeGen.Tests/CodeGenNetInteropTests.cs index 8480f7aa..d3c8a203 100644 --- a/Cesium.CodeGen.Tests/CodeGenNetInteropTests.cs +++ b/Cesium.CodeGen.Tests/CodeGenNetInteropTests.cs @@ -170,4 +170,22 @@ int main(void) return Func(&myFunc) - 1; } """); + + [Theory] + [InlineData(TargetArchitectureSet.Dynamic)] + [InlineData(TargetArchitectureSet.Wide)] + public Task TestEquivalentTypeAttribute(TargetArchitectureSet architecture) => DoTest(architecture, +@"using Cesium.Runtime; +public static unsafe class Test +{ + public static int Func(UTF8String str) => (int)str.Length; +}", +@" +__cli_import(""Test::Func"") +int Func(char*); + +int main(void) +{ + return Func(""Hi"") - 2; +}"); } diff --git a/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.SignedByteArrayTest.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.SignedByteArrayTest.verified.txt new file mode 100644 index 00000000..9793ad88 --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.SignedByteArrayTest.verified.txt @@ -0,0 +1,37 @@ +System.Int32 ::main() + Locals: + System.SByte* V_0 + System.SByte V_1 + System.Int32 V_2 + IL_0000: ldc.i4.1 + IL_0001: conv.u + IL_0002: localloc + IL_0004: stloc.0 + IL_0005: ldsflda / ::ConstDataBuffer0 + IL_000a: ldloc V_0 + IL_000e: ldc.i4.1 + IL_000f: conv.u + IL_0010: call System.Void Cesium.Runtime.RuntimeHelpers::InitializeCompound(System.Void*,System.Void*,System.UInt32) + IL_0015: ldloc.0 + IL_0016: ldc.i4.0 + IL_0017: conv.i + IL_0018: ldc.i4 1 + IL_001d: mul + IL_001e: add + IL_001f: ldind.i1 + IL_0020: stloc.1 + IL_0021: ldloc.1 + IL_0022: conv.i4 + IL_0023: stloc.2 + IL_0024: ldloc.2 + IL_0025: ret + +System.Int32 ::() + Locals: + System.Int32 V_0 + IL_0000: call System.Int32 ::main() + IL_0005: stloc.s V_0 + IL_0007: ldloc.s V_0 + IL_0009: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_000e: ldloc.s V_0 + IL_0010: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.UnSignedByteArrayTest2.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.UnSignedByteArrayTest2.verified.txt new file mode 100644 index 00000000..8950ed17 --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenArrayTests.UnSignedByteArrayTest2.verified.txt @@ -0,0 +1,37 @@ +System.Int32 ::main() + Locals: + System.Byte* V_0 + System.Byte V_1 + System.Int32 V_2 + IL_0000: ldc.i4.1 + IL_0001: conv.u + IL_0002: localloc + IL_0004: stloc.0 + IL_0005: ldsflda / ::ConstDataBuffer0 + IL_000a: ldloc V_0 + IL_000e: ldc.i4.1 + IL_000f: conv.u + IL_0010: call System.Void Cesium.Runtime.RuntimeHelpers::InitializeCompound(System.Void*,System.Void*,System.UInt32) + IL_0015: ldloc.0 + IL_0016: ldc.i4.0 + IL_0017: conv.i + IL_0018: ldc.i4 1 + IL_001d: mul + IL_001e: add + IL_001f: ldind.u1 + IL_0020: stloc.1 + IL_0021: ldloc.1 + IL_0022: conv.i4 + IL_0023: stloc.2 + IL_0024: ldloc.2 + IL_0025: ret + +System.Int32 ::() + Locals: + System.Int32 V_0 + IL_0000: call System.Int32 ::main() + IL_0005: stloc.s V_0 + IL_0007: ldloc.s V_0 + IL_0009: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_000e: ldloc.s V_0 + IL_0010: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstArgcMain.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstArgcMain.verified.txt new file mode 100644 index 00000000..5fbf42bb --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstArgcMain.verified.txt @@ -0,0 +1,43 @@ +System.Int32 ::main(System.Int32 argc, System.Byte** argv) + IL_0000: ldc.i4.0 + IL_0001: ret + +System.Int32 ::(System.String[] args) + Locals: + System.Int32 V_0 + System.Byte*[] V_1 + System.Byte*[] V_2 + System.Byte*[] (pinned) V_3 + System.Int32 V_4 + IL_0000: ldarg.0 + IL_0001: ldlen + IL_0002: ldc.i4.1 + IL_0003: add + IL_0004: stloc.0 + IL_0005: ldarg.0 + IL_0006: call System.Byte*[] Cesium.Runtime.RuntimeHelpers::ArgsToArgv(System.String[]) + IL_000b: stloc.1 + IL_000c: ldloc.1 + IL_000d: ldlen + IL_000e: newarr System.Byte* + IL_0013: stloc.2 + IL_0014: ldloc.1 + IL_0015: ldloc.2 + IL_0016: ldc.i4.0 + IL_0017: call System.Void System.Array::CopyTo(System.Array,System.Int32) + IL_001c: ldloc.0 + IL_001d: ldloc.2 + IL_001e: stloc.3 + IL_001f: ldloc.3 + IL_0020: ldc.i4.0 + IL_0021: ldelema System.Byte* + IL_0026: call System.Int32 ::main(System.Int32,System.Byte**) + IL_002b: stloc.s V_4 + IL_002d: ldnull + IL_002e: stloc.3 + IL_002f: ldloc.1 + IL_0030: call System.Void Cesium.Runtime.RuntimeHelpers::FreeArgv(System.Byte*[]) + IL_0035: ldloc.s V_4 + IL_0037: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_003c: ldloc.s V_4 + IL_003e: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstConstMain.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstConstMain.verified.txt new file mode 100644 index 00000000..5fbf42bb --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.ConstConstMain.verified.txt @@ -0,0 +1,43 @@ +System.Int32 ::main(System.Int32 argc, System.Byte** argv) + IL_0000: ldc.i4.0 + IL_0001: ret + +System.Int32 ::(System.String[] args) + Locals: + System.Int32 V_0 + System.Byte*[] V_1 + System.Byte*[] V_2 + System.Byte*[] (pinned) V_3 + System.Int32 V_4 + IL_0000: ldarg.0 + IL_0001: ldlen + IL_0002: ldc.i4.1 + IL_0003: add + IL_0004: stloc.0 + IL_0005: ldarg.0 + IL_0006: call System.Byte*[] Cesium.Runtime.RuntimeHelpers::ArgsToArgv(System.String[]) + IL_000b: stloc.1 + IL_000c: ldloc.1 + IL_000d: ldlen + IL_000e: newarr System.Byte* + IL_0013: stloc.2 + IL_0014: ldloc.1 + IL_0015: ldloc.2 + IL_0016: ldc.i4.0 + IL_0017: call System.Void System.Array::CopyTo(System.Array,System.Int32) + IL_001c: ldloc.0 + IL_001d: ldloc.2 + IL_001e: stloc.3 + IL_001f: ldloc.3 + IL_0020: ldc.i4.0 + IL_0021: ldelema System.Byte* + IL_0026: call System.Int32 ::main(System.Int32,System.Byte**) + IL_002b: stloc.s V_4 + IL_002d: ldnull + IL_002e: stloc.3 + IL_002f: ldloc.1 + IL_0030: call System.Void Cesium.Runtime.RuntimeHelpers::FreeArgv(System.Byte*[]) + IL_0035: ldloc.s V_4 + IL_0037: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_003c: ldloc.s V_4 + IL_003e: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.PointerPointerMain.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.PointerPointerMain.verified.txt new file mode 100644 index 00000000..5fbf42bb --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.PointerPointerMain.verified.txt @@ -0,0 +1,43 @@ +System.Int32 ::main(System.Int32 argc, System.Byte** argv) + IL_0000: ldc.i4.0 + IL_0001: ret + +System.Int32 ::(System.String[] args) + Locals: + System.Int32 V_0 + System.Byte*[] V_1 + System.Byte*[] V_2 + System.Byte*[] (pinned) V_3 + System.Int32 V_4 + IL_0000: ldarg.0 + IL_0001: ldlen + IL_0002: ldc.i4.1 + IL_0003: add + IL_0004: stloc.0 + IL_0005: ldarg.0 + IL_0006: call System.Byte*[] Cesium.Runtime.RuntimeHelpers::ArgsToArgv(System.String[]) + IL_000b: stloc.1 + IL_000c: ldloc.1 + IL_000d: ldlen + IL_000e: newarr System.Byte* + IL_0013: stloc.2 + IL_0014: ldloc.1 + IL_0015: ldloc.2 + IL_0016: ldc.i4.0 + IL_0017: call System.Void System.Array::CopyTo(System.Array,System.Int32) + IL_001c: ldloc.0 + IL_001d: ldloc.2 + IL_001e: stloc.3 + IL_001f: ldloc.3 + IL_0020: ldc.i4.0 + IL_0021: ldelema System.Byte* + IL_0026: call System.Int32 ::main(System.Int32,System.Byte**) + IL_002b: stloc.s V_4 + IL_002d: ldnull + IL_002e: stloc.3 + IL_002f: ldloc.1 + IL_0030: call System.Void Cesium.Runtime.RuntimeHelpers::FreeArgv(System.Byte*[]) + IL_0035: ldloc.s V_4 + IL_0037: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_003c: ldloc.s V_4 + IL_003e: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.UnaryPlusAndStart.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.UnaryPlusAndStart.verified.txt new file mode 100644 index 00000000..7e322293 --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenMethodTests.UnaryPlusAndStart.verified.txt @@ -0,0 +1,33 @@ +System.Int32 ::main() + Locals: + System.Int16 V_0 + System.Int16* V_1 + System.Int16 V_2 + System.Int32 V_3 + IL_0000: ldc.i4.s -2 + IL_0002: conv.i2 + IL_0003: stloc.0 + IL_0004: ldloca.s V_0 + IL_0006: stloc.1 + IL_0007: ldloc.1 + IL_0008: ldind.i2 + IL_0009: stloc.2 + IL_000a: ldloc.2 + IL_000b: conv.i4 + IL_000c: ldc.i4.1 + IL_000d: sub + IL_000e: ldc.i4.m1 + IL_000f: sub + IL_0010: stloc.3 + IL_0011: sizeof System.Int32 + IL_0017: ret + +System.Int32 ::() + Locals: + System.Int32 V_0 + IL_0000: call System.Int32 ::main() + IL_0005: stloc.s V_0 + IL_0007: ldloc.s V_0 + IL_0009: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_000e: ldloc.s V_0 + IL_0010: ret diff --git a/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Dynamic.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Dynamic.verified.txt new file mode 100644 index 00000000..1db6d463 --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Dynamic.verified.txt @@ -0,0 +1,29 @@ +Module: Primary + Type: + Methods: + System.Int32 ::main() + IL_0000: ldsflda / ::ConstDataBuffer0 + IL_0005: call Cesium.Runtime.UTF8String Cesium.Runtime.UTF8String::op_Implicit(System.Byte*) + IL_000a: call System.Int32 Test::Func(Cesium.Runtime.UTF8String) + IL_000f: ldc.i4.2 + IL_0010: sub + IL_0011: ret + + System.Int32 ::() + Locals: + System.Int32 V_0 + IL_0000: call System.Int32 ::main() + IL_0005: stloc.s V_0 + IL_0007: ldloc.s V_0 + IL_0009: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_000e: ldloc.s V_0 + IL_0010: ret + + Type: + Nested types: + Type: / + Pack: 1 + Size: 3 + Fields: + / ::ConstDataBuffer0 + Init with (UTF-8 x 3 bytes): "Hi" diff --git a/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Wide.verified.txt b/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Wide.verified.txt new file mode 100644 index 00000000..1db6d463 --- /dev/null +++ b/Cesium.CodeGen.Tests/verified/CodeGenNetInteropTests.TestEquivalentTypeAttribute_architecture=Wide.verified.txt @@ -0,0 +1,29 @@ +Module: Primary + Type: + Methods: + System.Int32 ::main() + IL_0000: ldsflda / ::ConstDataBuffer0 + IL_0005: call Cesium.Runtime.UTF8String Cesium.Runtime.UTF8String::op_Implicit(System.Byte*) + IL_000a: call System.Int32 Test::Func(Cesium.Runtime.UTF8String) + IL_000f: ldc.i4.2 + IL_0010: sub + IL_0011: ret + + System.Int32 ::() + Locals: + System.Int32 V_0 + IL_0000: call System.Int32 ::main() + IL_0005: stloc.s V_0 + IL_0007: ldloc.s V_0 + IL_0009: call System.Void Cesium.Runtime.RuntimeHelpers::Exit(System.Int32) + IL_000e: ldloc.s V_0 + IL_0010: ret + + Type: + Nested types: + Type: / + Pack: 1 + Size: 3 + Fields: + / ::ConstDataBuffer0 + Init with (UTF-8 x 3 bytes): "Hi" diff --git a/Cesium.CodeGen/Cesium.CodeGen.csproj b/Cesium.CodeGen/Cesium.CodeGen.csproj index 9adc88e7..dc7c4b3e 100644 --- a/Cesium.CodeGen/Cesium.CodeGen.csproj +++ b/Cesium.CodeGen/Cesium.CodeGen.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable true diff --git a/Cesium.CodeGen/Contexts/AssemblyContext.cs b/Cesium.CodeGen/Contexts/AssemblyContext.cs index 7fb74ace..fb8f8f77 100644 --- a/Cesium.CodeGen/Contexts/AssemblyContext.cs +++ b/Cesium.CodeGen/Contexts/AssemblyContext.cs @@ -299,7 +299,7 @@ private TypeReference GetStubType(int size) "", stubStructTypeName, TypeAttributes.Sealed | TypeAttributes.ExplicitLayout | TypeAttributes.NestedPrivate, - Module.ImportReference(MscorlibAssembly.GetType("System.ValueType"))) + Module.ImportReference(new TypeReference("System", "ValueType", MscorlibAssembly.MainModule, MscorlibAssembly.MainModule.TypeSystem.CoreLibrary))) { PackingSize = 1, ClassSize = size diff --git a/Cesium.CodeGen/Extensions/TypeSystemEx.cs b/Cesium.CodeGen/Extensions/TypeSystemEx.cs index 720ac72e..ef9110a7 100644 --- a/Cesium.CodeGen/Extensions/TypeSystemEx.cs +++ b/Cesium.CodeGen/Extensions/TypeSystemEx.cs @@ -13,6 +13,7 @@ internal static class TypeSystemEx public const string CPtrFullTypeName = "Cesium.Runtime.CPtr`1"; public const string VoidPtrFullTypeName = "Cesium.Runtime.VoidPtr"; public const string FuncPtrFullTypeName = "Cesium.Runtime.FuncPtr`1"; + public const string EquivalentTypeAttributeName = "Cesium.Runtime.Attributes.EquivalentTypeAttribute"; public static MethodReference MethodLookup( this TranslationUnitContext context, @@ -160,6 +161,21 @@ private static bool TypesCorrespond(TypeSystem typeSystem, TypeReference type1, return type1 is PointerType pt && pt.ElementType.IsEqualTo(typeSystem.Void); } + var resolvedType2 = type2.Resolve(); + if (resolvedType2.HasCustomAttributes) + { + // check for EquivalentTypeAttribute + foreach(var attr in resolvedType2.CustomAttributes) + { + if (!attr.AttributeType.FullName.Equals(EquivalentTypeAttributeName)) + continue; + + var eqType = (TypeReference)attr.ConstructorArguments[0].Value; + if (type1.FullName == eqType.FullName) + return true; + } + } + if (type2 is not GenericInstanceType type2Instance) return false; var type2Definition = type2.GetElementType(); if (type1.IsPointer) diff --git a/Cesium.CodeGen/Ir/BlockItems/FunctionDefinition.cs b/Cesium.CodeGen/Ir/BlockItems/FunctionDefinition.cs index 8a46f03a..0cbafe9c 100644 --- a/Cesium.CodeGen/Ir/BlockItems/FunctionDefinition.cs +++ b/Cesium.CodeGen/Ir/BlockItems/FunctionDefinition.cs @@ -125,12 +125,16 @@ private MethodDefinition GenerateSyntheticEntryPoint( bool isValid = true; var argc = parameterList[0]; - if (argc.Type is not PrimitiveType { Kind: PrimitiveTypeKind.Int }) isValid = false; + if (argc.Type is not PrimitiveType { Kind: PrimitiveTypeKind.Int } // int argc + and not ConstType { Base: PrimitiveType { Kind: PrimitiveTypeKind.Int } /* const int argc */ }) isValid = false; var argv = parameterList[1]; - if (argv.Type is not PointerType + if (argv.Type is not PointerType // char** or char*[] { Base: PointerType { Base: PrimitiveType { Kind: PrimitiveTypeKind.Char } } + } and not PointerType // [opt const] char * const * + { + Base: PointerType { Base: ConstType { Base: PrimitiveType { Kind: PrimitiveTypeKind.Char } } } }) isValid = false; if (!isValid) diff --git a/Cesium.CodeGen/Ir/Expressions/FunctionCallExpression.cs b/Cesium.CodeGen/Ir/Expressions/FunctionCallExpression.cs index 8bdda7ae..3a75c28c 100644 --- a/Cesium.CodeGen/Ir/Expressions/FunctionCallExpression.cs +++ b/Cesium.CodeGen/Ir/Expressions/FunctionCallExpression.cs @@ -124,11 +124,12 @@ public override void EmitTo(IEmitScope scope) if (_callee == null) throw new AssertException("Should be lowered"); - EmitArgumentList(scope, _callee.Parameters, _arguments); - var functionName = _function.Identifier; var callee = _callee ?? throw new CompilationException($"Function \"{functionName}\" was not lowered."); var methodReference = callee.MethodReference ?? throw new CompilationException($"Function \"{functionName}\" was not found."); + + EmitArgumentList(scope, _callee.Parameters, _arguments, methodReference); + scope.Method.Body.Instructions.Add(Instruction.Create(OpCodes.Call, methodReference)); } diff --git a/Cesium.CodeGen/Ir/Expressions/FunctionCallExpressionBase.cs b/Cesium.CodeGen/Ir/Expressions/FunctionCallExpressionBase.cs index 79b51909..8f7abe36 100644 --- a/Cesium.CodeGen/Ir/Expressions/FunctionCallExpressionBase.cs +++ b/Cesium.CodeGen/Ir/Expressions/FunctionCallExpressionBase.cs @@ -1,6 +1,7 @@ using Cesium.CodeGen.Contexts; using Cesium.CodeGen.Extensions; using Cesium.CodeGen.Ir.Types; +using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; @@ -12,7 +13,7 @@ internal abstract class FunctionCallExpressionBase : IExpression public abstract void EmitTo(IEmitScope scope); public abstract IType GetExpressionType(IDeclarationScope scope); - protected void EmitArgumentList(IEmitScope scope, ParametersInfo? paramInfo, IReadOnlyList arguments) + protected void EmitArgumentList(IEmitScope scope, ParametersInfo? paramInfo, IReadOnlyList arguments, MethodReference? method = null) { var explicitParametersCount = paramInfo?.Parameters.Count ?? 0; var varArgParametersCount = arguments.Count - explicitParametersCount; @@ -39,8 +40,28 @@ protected void EmitArgumentList(IEmitScope scope, ParametersInfo? paramInfo, IRe scope.AddInstruction(OpCodes.Stloc, varArgBuffer); } + var counter = 0; foreach (var argument in arguments.Take(explicitParametersCount)) + { argument.EmitTo(scope); + if (paramInfo?.IsVarArg != true && method != null) + { + var passedArg = argument.GetExpressionType((IDeclarationScope)scope).Resolve(scope.Context); + var actualArg = method.Parameters[counter].ParameterType; + counter++; + if (passedArg.FullName != actualArg.FullName) + { + //var conversion = actualArg.Resolve().Methods.FirstOrDefault(method => method.Name == "op_Implicit" && + // method.ReturnType == actualArg && method.Parameters[0].ParameterType == passedArg); <--- exception :( + //if (conversion == null) + // continue; + var conversion = new MethodReference("op_Implicit", actualArg, actualArg); // Gentlemen are taken at their word. + conversion.Parameters.Add(new(passedArg)); + + scope.AddInstruction(OpCodes.Call, conversion); + } + } + } if (paramInfo?.IsVarArg == true) { diff --git a/Cesium.CodeGen/Ir/Expressions/UnaryOperator.cs b/Cesium.CodeGen/Ir/Expressions/UnaryOperator.cs index 7a2baa85..cce75e94 100644 --- a/Cesium.CodeGen/Ir/Expressions/UnaryOperator.cs +++ b/Cesium.CodeGen/Ir/Expressions/UnaryOperator.cs @@ -3,6 +3,7 @@ namespace Cesium.CodeGen.Ir.Expressions; public enum UnaryOperator { Negation, // - + Promotion, // + BitwiseNot, // ~ LogicalNot, // ! AddressOf, // & diff --git a/Cesium.CodeGen/Ir/Expressions/UnaryOperatorExpression.cs b/Cesium.CodeGen/Ir/Expressions/UnaryOperatorExpression.cs index cabbd90f..01c961cc 100644 --- a/Cesium.CodeGen/Ir/Expressions/UnaryOperatorExpression.cs +++ b/Cesium.CodeGen/Ir/Expressions/UnaryOperatorExpression.cs @@ -55,6 +55,11 @@ public void EmitTo(IEmitScope scope) scope.Method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_0)); scope.Method.Body.Instructions.Add(Instruction.Create(OpCodes.Ceq)); break; + case UnaryOperator.Promotion: + Target.EmitTo(scope); + if (ShouldBePromoted(Target.GetExpressionType((IDeclarationScope)scope))) + scope.Method.Body.Instructions.Add(Instruction.Create(OpCodes.Conv_I4)); + break; default: Target.EmitTo(scope); scope.Method.Body.Instructions.Add(GetInstruction()); @@ -72,12 +77,17 @@ public void EmitTo(IEmitScope scope) public IType GetExpressionType(IDeclarationScope scope) => Operator switch { UnaryOperator.AddressOf => Target.GetExpressionType(scope).MakePointerType(), // address-of returns T* + UnaryOperator.Promotion => ShouldBePromoted(Target.GetExpressionType(scope)) ? new PrimitiveType(PrimitiveTypeKind.Int) : Target.GetExpressionType(scope), _ => Target.GetExpressionType(scope), // other operators return T }; + // TargetArchitectureSet is not important. Short way to check for int, short, byte. + private static bool ShouldBePromoted(IType input) => input is PrimitiveType pt && (pt.GetSizeInBytes(TargetArchitectureSet.Bit32)! < 4); + private static UnaryOperator GetOperatorKind(string @operator) => @operator switch { "-" => UnaryOperator.Negation, + "+" => UnaryOperator.Promotion, "!" => UnaryOperator.LogicalNot, "~" => UnaryOperator.BitwiseNot, "&" => UnaryOperator.AddressOf, diff --git a/Cesium.CodeGen/Ir/Types/InPlaceArrayType.cs b/Cesium.CodeGen/Ir/Types/InPlaceArrayType.cs index 421eaad7..9965a11a 100644 --- a/Cesium.CodeGen/Ir/Types/InPlaceArrayType.cs +++ b/Cesium.CodeGen/Ir/Types/InPlaceArrayType.cs @@ -37,8 +37,8 @@ public FieldDefinition CreateFieldOfType(TranslationUnitContext context, TypeDef CustomAttribute GenerateCustomFieldAttribute() { - var typeType = context.Module.ImportReference(context.AssemblyContext.MscorlibAssembly.GetType("System.Type")); - var fixedBufferAttributeType = context.AssemblyContext.MscorlibAssembly.GetType("System.Runtime.CompilerServices.FixedBufferAttribute") ?? throw new AssertException( + var typeType = context.Module.ImportReference(new TypeReference("System", "Type", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary)); + var fixedBufferAttributeType = new TypeReference("System.Runtime.CompilerServices", "FixedBufferAttribute", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary) ?? throw new AssertException( "Cannot find a type System.Runtime.CompilerServices.FixedBufferAttribute."); var fixedBufferCtor = new MethodReference(".ctor", context.TypeSystem.Void, fixedBufferAttributeType); fixedBufferCtor.Parameters.Add(new ParameterDefinition(typeType)); @@ -104,12 +104,12 @@ private static TypeDefinition CreateFixedBufferType( // } ModuleDefinition module = context.Module; - var compilerGeneratedAttributeType = context.AssemblyContext.MscorlibAssembly.GetType("System.Runtime.CompilerServices.CompilerGeneratedAttribute") ?? throw new AssertException( + var compilerGeneratedAttributeType = new TypeReference("System.Runtime.CompilerServices", "CompilerGeneratedAttribute", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary) ?? throw new AssertException( "Cannot find a type System.Runtime.CompilerServices.CompilerGeneratedAttribute."); var compilerGeneratedCtor = new MethodReference(".ctor", context.TypeSystem.Void, compilerGeneratedAttributeType); var compilerGeneratedAttribute = new CustomAttribute(module.ImportReference(compilerGeneratedCtor)); - var unsafeValueTypeAttributeType = context.AssemblyContext.MscorlibAssembly.GetType("System.Runtime.CompilerServices.UnsafeValueTypeAttribute") ?? throw new AssertException( + var unsafeValueTypeAttributeType = new TypeReference("System.Runtime.CompilerServices", "UnsafeValueTypeAttribute", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary) ?? throw new AssertException( "Cannot find a type System.Runtime.CompilerServices.UnsafeValueTypeAttribute."); var unsafeValueTypeCtor = new MethodReference(".ctor", context.TypeSystem.Void, unsafeValueTypeAttributeType); var unsafeValueTypeAttribute = new CustomAttribute(module.ImportReference(unsafeValueTypeCtor)); @@ -118,7 +118,7 @@ private static TypeDefinition CreateFixedBufferType( "", $"{fieldName}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout | TypeAttributes.NestedPublic, - module.ImportReference(context.AssemblyContext.MscorlibAssembly.GetType("System.ValueType"))) + module.ImportReference(new TypeReference("System", "ValueType", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary))) { PackingSize = 0, ClassSize = sizeInBytes, diff --git a/Cesium.CodeGen/Ir/Types/PrimitiveType.cs b/Cesium.CodeGen/Ir/Types/PrimitiveType.cs index 7a2c3686..bd4e2fe0 100644 --- a/Cesium.CodeGen/Ir/Types/PrimitiveType.cs +++ b/Cesium.CodeGen/Ir/Types/PrimitiveType.cs @@ -188,9 +188,9 @@ internal static class PrimitiveTypeInfo { PrimitiveTypeKind.SignedChar, (OpCodes.Ldind_I1, OpCodes.Stind_I1) }, { PrimitiveTypeKind.UnsignedChar, (OpCodes.Ldind_U1, OpCodes.Stind_I1) }, { PrimitiveTypeKind.Short, (OpCodes.Ldind_I2, OpCodes.Stind_I2) }, - { PrimitiveTypeKind.UnsignedShort, (OpCodes.Ldind_I2, OpCodes.Stind_I2) }, + { PrimitiveTypeKind.UnsignedShort, (OpCodes.Ldind_U2, OpCodes.Stind_I2) }, { PrimitiveTypeKind.Int, (OpCodes.Ldind_I4, OpCodes.Stind_I4) }, - { PrimitiveTypeKind.UnsignedInt, (OpCodes.Ldind_I4, OpCodes.Stind_I4) }, + { PrimitiveTypeKind.UnsignedInt, (OpCodes.Ldind_U4, OpCodes.Stind_I4) }, { PrimitiveTypeKind.Float, (OpCodes.Ldind_R4, OpCodes.Stind_R4) }, { PrimitiveTypeKind.Long, (OpCodes.Ldind_I8, OpCodes.Stind_I8) }, { PrimitiveTypeKind.UnsignedLong, (OpCodes.Ldind_I8, OpCodes.Stind_I8) }, diff --git a/Cesium.CodeGen/Ir/Types/StructType.cs b/Cesium.CodeGen/Ir/Types/StructType.cs index 069af96a..71e9a47a 100644 --- a/Cesium.CodeGen/Ir/Types/StructType.cs +++ b/Cesium.CodeGen/Ir/Types/StructType.cs @@ -42,7 +42,7 @@ public TypeDefinition StartEmit(string name, TranslationUnitContext context) "", Identifier is null ? "" + name : Identifier, TypeAttributes.Sealed, - context.Module.ImportReference(context.AssemblyContext.MscorlibAssembly.GetType("System.ValueType"))); + context.Module.ImportReference(new TypeReference("System", "ValueType", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary))); switch (context.AssemblyContext.ArchitectureSet) { case TargetArchitectureSet.Dynamic: @@ -106,7 +106,7 @@ private void EmitAsAnonStructure(TranslationUnitContext context) string.Empty, CreateAnonIdentifier(Members, IsUnion), TypeAttributes.Public | TypeAttributes.Sealed, - context.Module.ImportReference(context.AssemblyContext.MscorlibAssembly.GetType("System.ValueType"))); + context.Module.ImportReference(new TypeReference("System", "ValueType", context.AssemblyContext.MscorlibAssembly.MainModule, context.AssemblyContext.MscorlibAssembly.MainModule.TypeSystem.CoreLibrary))); FinishEmit(type, type.Name, context); // emit fields diff --git a/Cesium.Compiler.Tests/Cesium.Compiler.Tests.csproj b/Cesium.Compiler.Tests/Cesium.Compiler.Tests.csproj index 297bb799..5667e066 100644 --- a/Cesium.Compiler.Tests/Cesium.Compiler.Tests.csproj +++ b/Cesium.Compiler.Tests/Cesium.Compiler.Tests.csproj @@ -15,10 +15,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Cesium.Compiler/Cesium.Compiler.csproj b/Cesium.Compiler/Cesium.Compiler.csproj index 9b78d196..be7246ac 100644 --- a/Cesium.Compiler/Cesium.Compiler.csproj +++ b/Cesium.Compiler/Cesium.Compiler.csproj @@ -4,9 +4,10 @@ Exe net8.0 enable - true true true + Major + win-x64;win-x86;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 diff --git a/Cesium.Compiler/FileSystemIncludeContext.cs b/Cesium.Compiler/FileSystemIncludeContext.cs index cdb7aebb..970a42d0 100644 --- a/Cesium.Compiler/FileSystemIncludeContext.cs +++ b/Cesium.Compiler/FileSystemIncludeContext.cs @@ -1,23 +1,31 @@ using System.Collections.Immutable; +using System.Text; using Cesium.Preprocessor; namespace Cesium.Compiler; -public sealed class FileSystemIncludeContext : IIncludeContext +public sealed class FileSystemIncludeContext(string stdLibDirectory, IEnumerable currentDirectory) + : IIncludeContext { - private readonly string _stdLibDirectory; - private readonly ImmutableArray _userIncludeDirectories; + private readonly ImmutableArray _userIncludeDirectories = [..currentDirectory]; private readonly List _guardedIncludedFiles = new(); - public FileSystemIncludeContext(string stdLibDirectory, IEnumerable currentDirectory) + public override string ToString() { - _stdLibDirectory = stdLibDirectory; - _userIncludeDirectories = currentDirectory.ToImmutableArray(); + var result = new StringBuilder(); + result.AppendLine($"Standard library directory: \"{stdLibDirectory}\""); + result.Append("User include directories: [\n"); + foreach (var dir in _userIncludeDirectories) + { + result.Append($"\"{dir}\"\n"); + } + result.Append("]"); + return result.ToString(); } public string LookUpAngleBracedIncludeFile(string filePath) { - var path = Path.Combine(_stdLibDirectory, filePath); + var path = Path.Combine(stdLibDirectory, filePath); if (File.Exists(path)) return Path.GetFullPath(path); @@ -41,7 +49,7 @@ public string LookUpQuotedIncludeFile(string filePath) return Path.GetFullPath(path); } - path = Path.Combine(_stdLibDirectory, filePath); + path = Path.Combine(stdLibDirectory, filePath); return Path.GetFullPath(path); } diff --git a/Cesium.Compiler/Main.cs b/Cesium.Compiler/Main.cs index 2ccd37d2..f7098266 100644 --- a/Cesium.Compiler/Main.cs +++ b/Cesium.Compiler/Main.cs @@ -1,43 +1,52 @@ +using System.Diagnostics.CodeAnalysis; using Cesium.CodeGen; -using Cesium.Compiler; using Cesium.Core; using Mono.Cecil; -await CommandLineParser.ParseCommandLineArgs(args, new CompilerReporter(), async args => +namespace Cesium.Compiler; + +public static class Program +{ + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Arguments))] + public static async Task Main(string[] args) { - var targetArchitectureSet = args.TargetArchitectureSet; - var targetRuntime = args.Framework switch + return await CommandLineParser.ParseCommandLineArgs(args, new CompilerReporter(), async args => { - TargetFrameworkKind.NetFramework => TargetRuntimeDescriptor.Net48, - TargetFrameworkKind.NetStandard => TargetRuntimeDescriptor.NetStandard20, - _ => TargetRuntimeDescriptor.Net60 - }; + var targetArchitectureSet = args.TargetArchitectureSet; + var targetRuntime = args.Framework switch + { + TargetFrameworkKind.NetFramework => TargetRuntimeDescriptor.Net48, + TargetFrameworkKind.NetStandard => TargetRuntimeDescriptor.NetStandard20, + _ => TargetRuntimeDescriptor.Net60 + }; - var cesiumRuntime = args.CesiumCRuntime ?? Path.Combine(AppContext.BaseDirectory, "Cesium.Runtime.dll"); - var defaultImportsAssembly = args.DefaultImportAssemblies ?? Array.Empty(); + var cesiumRuntime = args.CesiumCRuntime ?? Path.Combine(AppContext.BaseDirectory, "Cesium.Runtime.dll"); + var defaultImportsAssembly = args.DefaultImportAssemblies ?? Array.Empty(); #pragma warning disable IL3000 // Automatic discovery of corelib is fallback option, if tooling do not pass that parameter - var corelibAssembly = args.CoreLib ?? typeof(Math).Assembly.Location; // System.Runtime.dll + var corelibAssembly = args.CoreLib ?? typeof(Math).Assembly.Location; // System.Runtime.dll #pragma warning restore IL3000 - var moduleKind = args.ProducePreprocessedFile ? ModuleKind.Console : args.ModuleKind ?? Path.GetExtension(args.OutputFilePath).ToLowerInvariant() switch - { - ".exe" => ModuleKind.Console, - ".dll" => ModuleKind.Dll, - var o => throw new CompilationException($"Unknown file extension: {o}. \"modulekind\" is not specified.") - }; - var compilationOptions = new CompilationOptions( - targetRuntime, - targetArchitectureSet, - moduleKind, - corelibAssembly, - cesiumRuntime, - defaultImportsAssembly, - args.Namespace, - args.GlobalClass, - args.DefineConstant.ToList(), - args.IncludeDirectories.ToList(), - args.ProducePreprocessedFile); - return await Compilation.Compile(args.InputFilePaths, args.OutputFilePath, compilationOptions); - }); + var moduleKind = args.ProducePreprocessedFile ? ModuleKind.Console : args.ModuleKind ?? Path.GetExtension(args.OutputFilePath).ToLowerInvariant() switch + { + ".exe" => ModuleKind.Console, + ".dll" => ModuleKind.Dll, + var o => throw new CompilationException($"Unknown file extension: {o}. \"modulekind\" is not specified.") + }; + var compilationOptions = new CompilationOptions( + targetRuntime, + targetArchitectureSet, + moduleKind, + corelibAssembly, + cesiumRuntime, + defaultImportsAssembly, + args.Namespace, + args.GlobalClass, + args.DefineConstant.ToList(), + args.IncludeDirectories.ToList(), + args.ProducePreprocessedFile); + return await Compilation.Compile(args.InputFilePaths, args.OutputFilePath, compilationOptions); + }); + } +} class CompilerReporter : ICompilerReporter { diff --git a/Cesium.Core/Cesium.Core.csproj b/Cesium.Core/Cesium.Core.csproj index aa68acee..7d989800 100644 --- a/Cesium.Core/Cesium.Core.csproj +++ b/Cesium.Core/Cesium.Core.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable diff --git a/Cesium.IntegrationTests/Cesium.IntegrationTests.csproj b/Cesium.IntegrationTests/Cesium.IntegrationTests.csproj index 9c69036e..c4bb6c58 100644 --- a/Cesium.IntegrationTests/Cesium.IntegrationTests.csproj +++ b/Cesium.IntegrationTests/Cesium.IntegrationTests.csproj @@ -12,7 +12,6 @@ - diff --git a/Cesium.IntegrationTests/IntegrationTestContext.cs b/Cesium.IntegrationTests/IntegrationTestContext.cs index fc1cd10a..dc37fcf5 100644 --- a/Cesium.IntegrationTests/IntegrationTestContext.cs +++ b/Cesium.IntegrationTests/IntegrationTestContext.cs @@ -1,6 +1,7 @@ using Cesium.TestFramework; using JetBrains.Annotations; using AsyncKeyedLock; +using Cesium.Solution.Metadata; using Xunit.Abstractions; namespace Cesium.IntegrationTests; @@ -72,7 +73,7 @@ public async ValueTask DisposeAsync() private static async Task BuildRuntime(ITestOutputHelper output) { var runtimeProjectFile = Path.Combine( - TestStructureUtil.SolutionRootPath, + SolutionMetadata.SourceRoot, "Cesium.Runtime/Cesium.Runtime.csproj"); await DotNetCliHelper.BuildDotNetProject(output, BuildConfiguration, runtimeProjectFile); } @@ -80,7 +81,7 @@ private static async Task BuildRuntime(ITestOutputHelper output) private static async Task BuildCompiler(ITestOutputHelper output) { var compilerProjectFile = Path.Combine( - TestStructureUtil.SolutionRootPath, + SolutionMetadata.SourceRoot, "Cesium.Compiler/Cesium.Compiler.csproj"); await DotNetCliHelper.BuildDotNetProject(output, BuildConfiguration, compilerProjectFile); } diff --git a/Cesium.IntegrationTests/IntegrationTestRunner.cs b/Cesium.IntegrationTests/IntegrationTestRunner.cs index c012fbe3..a2ba15ca 100644 --- a/Cesium.IntegrationTests/IntegrationTestRunner.cs +++ b/Cesium.IntegrationTests/IntegrationTestRunner.cs @@ -1,3 +1,4 @@ +using Cesium.Solution.Metadata; using Cesium.TestFramework; using Xunit.Abstractions; @@ -20,7 +21,7 @@ public Task InitializeAsync() => public static IEnumerable TestCaseProvider() { - var testCaseDirectory = Path.Combine(TestStructureUtil.SolutionRootPath, "Cesium.IntegrationTests"); + var testCaseDirectory = Path.Combine(SolutionMetadata.SourceRoot, "Cesium.IntegrationTests"); var cFiles = Directory.EnumerateFileSystemEntries(testCaseDirectory, "*.c", SearchOption.AllDirectories); return cFiles .Where(file => !file.EndsWith(".ignore.c")) @@ -61,7 +62,7 @@ private async Task DoTest(string relativeSourcePath, TargetFramework targetFrame Directory.CreateDirectory(objDirPath); var sourceFilePath = Path.Combine( - TestStructureUtil.SolutionRootPath, + SolutionMetadata.SourceRoot, "Cesium.IntegrationTests", relativeSourcePath); @@ -179,7 +180,7 @@ private async Task BuildExecutableWithCesium( "run", "--no-build", "--configuration", IntegrationTestContext.BuildConfiguration, - "--project", Path.Combine(TestStructureUtil.SolutionRootPath, "Cesium.Compiler"), + "--project", Path.Combine(SolutionMetadata.SourceRoot, "Cesium.Compiler"), "--", "--nologo", sourceFilePath, @@ -192,10 +193,10 @@ private async Task BuildExecutableWithCesium( { var coreLibPath = WindowsEnvUtil.MsCorLibPath; var runtimeLibPath = Path.Combine( - TestStructureUtil.SolutionRootPath, - "Cesium.Runtime/bin", - IntegrationTestContext.BuildConfiguration, - "netstandard2.0/Cesium.Runtime.dll" + SolutionMetadata.ArtifactsRoot, + "bin/Cesium.Runtime", + $"{IntegrationTestContext.BuildConfiguration.ToLower()}_netstandard2.0", + "Cesium.Runtime.dll" ); args.AddRange(new[] { diff --git a/Cesium.IntegrationTests/function_ptr.c b/Cesium.IntegrationTests/function_ptr.c index b5fc9d11..29632475 100644 --- a/Cesium.IntegrationTests/function_ptr.c +++ b/Cesium.IntegrationTests/function_ptr.c @@ -17,7 +17,7 @@ int main(void) return x(40); - // TODO [#196] + // TODO[#196] // foov_t y = &foov; // return (x(40) + y(40, 123, 456, "foo")) / 2; } diff --git a/Cesium.IntegrationTests/some_unaryops.c b/Cesium.IntegrationTests/some_unaryops.c new file mode 100644 index 00000000..2f133ea3 --- /dev/null +++ b/Cesium.IntegrationTests/some_unaryops.c @@ -0,0 +1,10 @@ +#include + +int main() { + short c = -2; + int x = 0; + long long d = 0; + printf("%d %d %d %d %d %d", c, +c, sizeof(c), sizeof(+c), sizeof(+x), sizeof(+d)); + // -2 -2 2 4 4 8 + return 42; +} diff --git a/Cesium.IntegrationTests/stdlib/printf.c b/Cesium.IntegrationTests/stdlib/printf.c index bce03742..9f99888b 100644 --- a/Cesium.IntegrationTests/stdlib/printf.c +++ b/Cesium.IntegrationTests/stdlib/printf.c @@ -21,9 +21,11 @@ int main(int argc, char *argv[]) return -4; } - if (printf("%lu\n", -1) != 11) { - return -5; - } + // TODO[#586]: This test is not portable: on Windows, Cesium and MSVC use different sizes for long. + // int maxULongLengthInChars = sizeof(long) == 4 ? 10 : 20; + // if (printf("%lu\n", -1L) != maxULongLengthInChars + 1) { // + 1 for \n + // return -5; + // } if (printf("%i\n", -1) != 3) { return -6; diff --git a/Cesium.Parser.Tests/Cesium.Parser.Tests.csproj b/Cesium.Parser.Tests/Cesium.Parser.Tests.csproj index 83d11c7f..ae428d01 100644 --- a/Cesium.Parser.Tests/Cesium.Parser.Tests.csproj +++ b/Cesium.Parser.Tests/Cesium.Parser.Tests.csproj @@ -15,10 +15,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Cesium.Parser/CParser.cs b/Cesium.Parser/CParser.cs index 2d3e8b60..da6889e5 100644 --- a/Cesium.Parser/CParser.cs +++ b/Cesium.Parser/CParser.cs @@ -182,7 +182,7 @@ private static Expression MakeUnaryOperatorExpression(ICToken @operator, Express : new UnaryOperatorExpression(@operator.Text, target); [Rule("unary_operator: '&'")] - // TODO[#207]: [Rule("unary_operator: '+'")] + [Rule("unary_operator: '+'")] [Rule("unary_operator: '-'")] [Rule("unary_operator: '~'")] [Rule("unary_operator: '!'")] diff --git a/Cesium.Parser/Cesium.Parser.csproj b/Cesium.Parser/Cesium.Parser.csproj index cade06f2..2894495a 100644 --- a/Cesium.Parser/Cesium.Parser.csproj +++ b/Cesium.Parser/Cesium.Parser.csproj @@ -1,8 +1,9 @@ - net7.0 + net8.0 enable + True diff --git a/Cesium.Parser/TokenExtensions.cs b/Cesium.Parser/TokenExtensions.cs index 7f0b5053..878438b2 100644 --- a/Cesium.Parser/TokenExtensions.cs +++ b/Cesium.Parser/TokenExtensions.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text; using Cesium.Core; using Yoakke.SynKit.C.Syntax; @@ -7,143 +8,158 @@ namespace Cesium.Parser; public static class TokenExtensions { - public static string UnwrapStringLiteral(this IToken token) + public unsafe static string UnwrapStringLiteral(this IToken token) { if (token.Kind != CTokenType.StringLiteral) throw new ParseException($"Non-string literal token: {token.Kind} {token.Text}"); - var result = token.Text[1..^1]; + var result = token.Text[1..^1]; // allocates new sliced string - // simple escape sequences - var builder = new StringBuilder(result.Length); - for (int i = 0; i < result.Length; ++i) + if (result.IndexOf('\\') == -1) // no \ no fun no unescape + return result; + + fixed (char* p = result) { - if (result.ElementAt(i) == '\\' && i < result.Length - 1) + var span = new Span(p, result.Length + 1); // create a span for string. Also +1 for \0 + int eaten = 0; + while (true) // loop { - char currentChar = result.ElementAt(i + 1); - switch (currentChar) + var i = span.IndexOf('\\'); // SIMD search \. Blazing fast. + if (i == -1) break; // break if there is no more \ + + int shift = 1; // how many characters we're gonna skip + + switch (span[i + 1]) { - case '\'': - case '\"': - case '?': - case '\\': - builder.Append(currentChar); - break; - case 'a': - builder.Append('\a'); - break; - case 'b': - builder.Append('\b'); - break; - case 'f': - builder.Append('\f'); - break; - case 'n': - builder.Append('\n'); - break; - case 'r': - builder.Append('\r'); - break; - case 't': - builder.Append('\t'); - break; - case 'v': - builder.Append('\v'); - break; - case '0': + // Simple escape sequences + case '"': span[i] = '"'; break; + case '?': span[i] = '?'; break; + case '\'': span[i] = '\''; break; + case '\\': span[i] = '\\'; break; + case 'a': span[i] = '\a'; break; + case 'b': span[i] = '\b'; break; + case 'f': span[i] = '\f'; break; + case 'n': span[i] = '\n'; break; + case 'r': span[i] = '\r'; break; + case 't': span[i] = '\t'; break; + case 'v': span[i] = '\v'; break; + // Numeric escape sequences + case '0': // arbitrary octal value '\nnn' { - int counter = 2; - int octalNumber = 0; - if (result.Length <= i + counter) + if (span.Length <= i + 2 || span[i + 2] == '\0') // \0 check for 2nd..n iters. { - builder.Append('\0'); + span[i] = '\0'; break; } - char current = result.ElementAt(i + counter); + int number = 0; + var c = span[i + shift + 1]; // get next char after 0 do { - octalNumber = octalNumber * 8 + (current - '0'); - counter++; - if (result.Length <= i + counter) - break; - current = result.ElementAt(i + counter); + number = number * 8 + (c - '0'); + shift++; + c = span[i + shift + 1]; } - while (current >= '0' && current <= '7'); - i += counter - 1; - builder.Append(char.ConvertFromUtf32(octalNumber)); + while (char.IsBetween(c, '0', '7')); + span[i] = (char)number; break; } case 'x': - case 'X': + case 'X': // \Xn... arbitrary hexadecimal value { - int counter = 2; - int octalNumber = 0; - if (result.Length <= i + counter) + if (span.Length <= i + 2 || span[i + 2] == '\0') // \0 check for 2nd..n iters. { - builder.Append('\\'); - builder.Append(currentChar); + shift = 0; break; } - char current = result.ElementAt(i + counter); + int number = 0; + var c = span[i + 1 + shift]; // shift == 1, so i + 2 points at next char after '\' 'X' do { - int digit = Char.IsAsciiDigit(current) ? current - '0' : (char.ToUpperInvariant(current) - 'A') + 10; - octalNumber = octalNumber * 16 + digit; - counter++; - if (result.Length <= i + counter) - break; - current = result.ElementAt(i + counter); + int digit = char.IsAsciiDigit(c) ? c - '0' : (char.ToUpperInvariant(c) - 'A') + 10; + number = number * 16 + digit; + shift++; + c = span[i + 1 + shift]; } - while (Char.IsAsciiDigit(current) || (current >= 'A' && current <= 'F') || (current >= 'a' && current <= 'f')); - i += counter - 1; - builder.Append((char)octalNumber); + while (char.IsAsciiDigit(c) || char.IsBetween(c, 'a', 'f') || char.IsBetween(c, 'A', 'F')); + span[i] = (char)number; break; } - case 'u': - case 'U': + // Universal character names + case 'u': // \unnnn + case 'U': // \Unnnnnnnn { - int counter = 2; - int octalNumber = 0; - if (result.Length <= i + counter) + int counter = span[i + 1] == 'U' ? 8 : 4; + if (span.Length <= i + counter) // no free chars no fun { - builder.Append('\\'); - builder.Append(currentChar); + shift = 0; break; } - char current = result.ElementAt(i + counter); - do + int number = 0; + for (int n = 0; n < counter; n++) { - int digit = Char.IsAsciiDigit(current) ? current - '0' : (char.ToUpperInvariant(current) - 'A') + 10; - octalNumber = octalNumber * 16 + digit; - counter++; - if (result.Length <= i + counter) - break; - current = result.ElementAt(i + counter); + var c = span[i + 2 + n]; // i + 2 points at next char after '\', 'U', + // in theory, we should throw an error. + if (!(char.IsAsciiDigit(c) || char.IsBetween(c, 'a', 'f') || char.IsBetween(c, 'A', 'F'))) break; + int digit = char.IsAsciiDigit(c) ? c - '0' : (char.ToUpperInvariant(c) - 'A') + 10; + number = number * 16 + digit; + shift++; } - while (Char.IsAsciiDigit(current) || (current >= 'A' && current <= 'F') || (current >= 'a' && current <= 'f')); - i += counter - 1; - builder.Append(char.ConvertFromUtf32(octalNumber)); + // char.ConvertFromUtf32(number) allocates string :( + // create span from pointer to our number represented as ref byte + var spanCodeSeq = new Span(Unsafe.AsPointer(ref Unsafe.As(ref number)), 4); + // get utf16 chars from utf32 bytes seq without allocations yeeeah + if (!Encoding.UTF32.TryGetChars(spanCodeSeq, span.Slice(i), out int written)) throw new Exception("Bad UTF32 sequence!"); + // if we writted one char, so just do nothing + // if we writted two chars, so just skip one char + i += written - 1; + shift -= written - 1; break; } default: + // from orig method: // TODO[#295]: maybe smarter handling of this edge case with errors/warnings - builder.Append('\\'); - --i; // don't skip next + // builder.Append('\\'); + // --i; // don't skip next + // mmm, idk when that might happen break; } - ++i; // skip next - } - else - { - builder.Append(result.ElementAt(i)); + if (shift == 0) + { + span = span.Slice(1); // skip 1 char + continue; + } + + // ex: + // before + // span[i] = '\' span[i+1] = 'n' span[i+2] = 'h' + // after + // shift = 1 + // span[i] = '\n' span[i+1] = 'n' span[i+2] = 'h' + // it is required to shift all subsequent chars by (1..shift) to the left + // result slice slice copy + // span[i] = '\n' span[i+1] = 'h' span[i+2] = (previous span)[i+3] + var start = i + shift; + var source = span.Slice(start + 1); // next from consumed chars + var destination = span.Slice(i + 1); // next from span[i] = '\n' + // If copy properly in chunks, rather than continuously copying the WHOLE unchecked string, unescape will be even faster + source.CopyTo(destination); // <--- copies the WHOLE unchecked string. bad. bad. but also SIMDed, so it's still very fast + span = destination; // next iter + eaten += shift; } - } - result = builder.ToString(); - return result; + // oh wait + // before: "kek \\n\\n\\n kek" len 17 + // after "kek \n\n\n kek\0\0\0" len 17 + // create copy & alocate it again + // we can change the length of an existing string, but GC always *bonk* for it + result = new ReadOnlySpan(p, result.Length - eaten).ToString(); // alocate x2 >< + // after: "kek \n\n\n kek" len 14 + + return result; + } } } diff --git a/Cesium.Preprocessor/CPreprocessor.cs b/Cesium.Preprocessor/CPreprocessor.cs index 66d7e4b9..6be1ab68 100644 --- a/Cesium.Preprocessor/CPreprocessor.cs +++ b/Cesium.Preprocessor/CPreprocessor.cs @@ -83,7 +83,7 @@ private async IAsyncEnumerable> ProcessGroupPart( { throw new PreprocessorException( filePathToken.Location, - $"Cannot find file {filePath} for include directive."); + $"Cannot find file {filePath} for include directive. Include context: {IncludeContext}"); } await foreach (var token in ProcessInclude(includeFilePath, reader)) { diff --git a/Cesium.Preprocessor/Cesium.Preprocessor.csproj b/Cesium.Preprocessor/Cesium.Preprocessor.csproj index 8fb1047f..fe7710f3 100644 --- a/Cesium.Preprocessor/Cesium.Preprocessor.csproj +++ b/Cesium.Preprocessor/Cesium.Preprocessor.csproj @@ -6,14 +6,14 @@ - - - - + + + + - + diff --git a/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj b/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj index 17108c41..82928fd8 100644 --- a/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj +++ b/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj @@ -16,10 +16,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Cesium.Runtime.Tests/PtrTests.cs b/Cesium.Runtime.Tests/PtrTests.cs index afae7533..83f43dc9 100644 --- a/Cesium.Runtime.Tests/PtrTests.cs +++ b/Cesium.Runtime.Tests/PtrTests.cs @@ -28,6 +28,15 @@ public void FuncPtrTests() { var a = new FuncPtr((void*)0x1234); Assert.Equal(0x1234L, (long)a.AsPtr()); - Assert.Equal(sizeof(long), sizeof(FuncPtr)); + Assert.Equal(sizeof(IntPtr), sizeof(FuncPtr)); + + FuncPtr> funcPtr = (Func)SomeAnonFunc; + var func = SomeAnonFunc; + Assert.Equal(funcPtr.AsDelegate()(), func()); + + funcPtr = (delegate*)&SomeAnonFunc; + Assert.Equal(funcPtr.AsDelegate()(), func()); + + static int SomeAnonFunc() => 5; } } diff --git a/Cesium.Runtime.Tests/StdIoFunctionTests.cs b/Cesium.Runtime.Tests/StdIoFunctionTests.cs index 25ff7ce7..82f90c7a 100644 --- a/Cesium.Runtime.Tests/StdIoFunctionTests.cs +++ b/Cesium.Runtime.Tests/StdIoFunctionTests.cs @@ -7,10 +7,28 @@ public class StdIoFunctionTests [Theory] [InlineData(9, "0x09")] [InlineData(32, "0x20")] - public unsafe void FPrintFHex(long input, string expectedResult) + public void FPrintFHex(long input, string expectedResult) { - var format = Encoding.UTF8.GetBytes("0x%02x"); + var (exitCode, result) = TestFPrintF("0x%02x", input); + Assert.Equal(expectedResult, result); + Assert.Equal(4, exitCode); + } + + [Theory] + [InlineData(-1L, "%li", 2, "-1")] + [InlineData(-1L, "%lu", 20, "18446744073709551615")] + public void FPrintFLong(long input, string format, int expectedExitCode, string expectedResult) + { + var (exitCode, result) = TestFPrintF(format, input); + Assert.Equal(expectedResult, result); + Assert.Equal(expectedExitCode, exitCode); + } + private static unsafe (int, string) TestFPrintF(string format, long input) + { + var formatEncoded = Encoding.UTF8.GetBytes(format); + + int exitCode; using var buffer = new MemoryStream(); var handleIndex = StdIoFunctions.Handles.Count; try @@ -24,9 +42,9 @@ public unsafe void FPrintFHex(long input, string expectedResult) }; StdIoFunctions.Handles.Add(handle); - fixed (byte* formatPtr = format) + fixed (byte* formatPtr = formatEncoded) { - Assert.Equal(4, StdIoFunctions.FPrintF((void*)handleIndex, formatPtr, &input)); + exitCode = StdIoFunctions.FPrintF((void*)handleIndex, formatPtr, &input); } } finally @@ -34,7 +52,6 @@ public unsafe void FPrintFHex(long input, string expectedResult) StdIoFunctions.Handles.RemoveAt(handleIndex); } - var result = Encoding.UTF8.GetString(buffer.ToArray()); - Assert.Equal(expectedResult, result); + return (exitCode, Encoding.UTF8.GetString(buffer.ToArray())); } } diff --git a/Cesium.Runtime.Tests/StringTests.cs b/Cesium.Runtime.Tests/StringTests.cs new file mode 100644 index 00000000..fbd77522 --- /dev/null +++ b/Cesium.Runtime.Tests/StringTests.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; + +namespace Cesium.Runtime.Tests; +public unsafe class StringTests +{ + [Fact] + public void ComplexTest() + { + Assert.Equal(sizeof(long), sizeof(UTF8String)); + + UTF8String someString = (byte*)Marshal.StringToHGlobalAnsi("Hello world!\0"); + + Assert.Equal("Hello world!".Length, (int)someString.Length); + Assert.Equal("Hello world!".Length + 1, (int)someString.NullTerminatedLength); + Assert.Equal("Hello world!", someString.ToString()); + + UTF8String someMemory = stackalloc byte[(int)someString.NullTerminatedLength]; + someString.CopyTo(someMemory); + Assert.Equal(someString.ToString(), someMemory.ToString()); + + UTF8String someOtherMemory = stackalloc byte[(int)someString.NullTerminatedLength]; + someString.CopyTo(someOtherMemory, 5); + someOtherMemory[6] = (byte)'\0'; + Assert.Equal("Hello", someOtherMemory.ToString()); + + Assert.Equal((byte)'o', someString.At(4)[0]); + } +} diff --git a/Cesium.Runtime/Attributes/EquivalentTypeAttribute.cs b/Cesium.Runtime/Attributes/EquivalentTypeAttribute.cs new file mode 100644 index 00000000..0d613d2c --- /dev/null +++ b/Cesium.Runtime/Attributes/EquivalentTypeAttribute.cs @@ -0,0 +1,7 @@ +namespace Cesium.Runtime.Attributes; + +[AttributeUsage(AttributeTargets.Struct)] +public class EquivalentTypeAttribute(Type equivalentType) : Attribute +{ + public Type EquivalentType { get; } = equivalentType; +} diff --git a/Cesium.Runtime/Cesium.Runtime.csproj b/Cesium.Runtime/Cesium.Runtime.csproj index d8fe30c4..38cce558 100644 --- a/Cesium.Runtime/Cesium.Runtime.csproj +++ b/Cesium.Runtime/Cesium.Runtime.csproj @@ -1,12 +1,13 @@ - - netstandard2.0;net6.0 - enable - enable - true - latest - + + netstandard2.0;net6.0 + enable + enable + true + latest + true + diff --git a/Cesium.Runtime/FuncPtr.cs b/Cesium.Runtime/FuncPtr.cs index 98460430..4310681b 100644 --- a/Cesium.Runtime/FuncPtr.cs +++ b/Cesium.Runtime/FuncPtr.cs @@ -1,7 +1,9 @@ +using System.Runtime.InteropServices; + namespace Cesium.Runtime; /// A class encapsulating a C function pointer. -public readonly unsafe struct FuncPtr where TDelegate : Delegate // TODO[#487]: Think about vararg and empty parameter list encoding. +public readonly unsafe struct FuncPtr where TDelegate : MulticastDelegate // TODO[#487]: Think about vararg and empty parameter list encoding. { private readonly long _value; @@ -10,5 +12,12 @@ public FuncPtr(void* ptr) _value = (long)ptr; } + public static implicit operator TDelegate(FuncPtr funcPtr) => (TDelegate)Activator.CreateInstance(typeof(TDelegate), [null, (IntPtr)funcPtr._value])!; + public static implicit operator FuncPtr(TDelegate @delegate) => @delegate.Method.MethodHandle.GetFunctionPointer(); + public static implicit operator FuncPtr(IntPtr funcPtr) => new((void*)funcPtr); + public static implicit operator FuncPtr(void* funcPtr) => new(funcPtr); + + public TDelegate AsDelegate() => this; + public void* AsPtr() => (void*)_value; } diff --git a/Cesium.Runtime/StdIoFunctions.cs b/Cesium.Runtime/StdIoFunctions.cs index 4ecac607..9f7ac05d 100644 --- a/Cesium.Runtime/StdIoFunctions.cs +++ b/Cesium.Runtime/StdIoFunctions.cs @@ -298,7 +298,6 @@ public static int FPrintF(void* stream, byte* str, void* varargs) case "d": case "ld": case "i": - case "li": int intValue = (int)((long*)varargs)[consumedArgs]; var intValueString = intValue.ToString(); if (alwaysSign && intValue > 0) @@ -319,16 +318,45 @@ public static int FPrintF(void* stream, byte* str, void* varargs) consumedBytes += intValueString.Length; consumedArgs++; break; - case "u": - case "lu": + case "li": + long longValue = ((long*)varargs)[consumedArgs]; + var longValueString = longValue.ToString(); + if (alwaysSign && longValue > 0) { - uint uintValue = (uint)((long*)varargs)[consumedArgs]; - var uintValueString = uintValue.ToString(); - streamWriter.Write(uintValueString); - consumedBytes += uintValueString.Length; - consumedArgs++; - break; + streamWriter.Write('+'); + } + + if (longValueString.Length < precision) + { + streamWriter.Write(new string('0', precision - longValueString.Length)); + } + + if (precision != 0 || longValue != 0) + { + streamWriter.Write(longValueString); } + + consumedBytes += longValueString.Length; + consumedArgs++; + break; + case "u": + { + uint uintValue = (uint)((long*)varargs)[consumedArgs]; + var uintValueString = uintValue.ToString(); + streamWriter.Write(uintValueString); + consumedBytes += uintValueString.Length; + consumedArgs++; + break; + } + case "lu": + { + ulong ulongValue = (ulong)((long*)varargs)[consumedArgs]; + var ulongValueString = ulongValue.ToString(); + streamWriter.Write(ulongValueString); + consumedBytes += ulongValueString.Length; + consumedArgs++; + break; + } case "f": { var floatNumber = ((double*)varargs)[consumedArgs]; diff --git a/Cesium.Runtime/StringFunctions.cs b/Cesium.Runtime/StringFunctions.cs index df755188..182912b2 100644 --- a/Cesium.Runtime/StringFunctions.cs +++ b/Cesium.Runtime/StringFunctions.cs @@ -12,81 +12,32 @@ namespace Cesium.Runtime; /// public static unsafe class StringFunctions { - public static nuint StrLen(byte* str) - { -#if NETSTANDARD - if (str == null) - { - return 0; - } + public static nuint StrLen(UTF8String str) => str.Length; - Encoding encoding = Encoding.UTF8; - int byteLength = 0; - byte* search = str; - while (*search != '\0') - { - byteLength++; - search++; - } - - int stringLength = encoding.GetCharCount(str, byteLength); - return (uint)stringLength; -#else - return (uint)(Marshal.PtrToStringUTF8((nint)str)?.Length ?? 0); -#endif - } - public static byte* StrCpy(byte* dest, byte* src) + public static byte* StrCpy(UTF8String dest, UTF8String src) { - if (dest == null) - { + if (!dest) return null; - } - var result = dest; - if (src == null) - { + if (!src) return dest; - } - byte* search = src; - while (*search != '\0') - { - *dest = *search; - search++; - dest++; - } + src.CopyTo(dest); - *dest = 0; - return result; + return dest; } - public static byte* StrNCpy(byte* dest, byte* src, nuint count) + + public static byte* StrNCpy(UTF8String dest, UTF8String src, nuint count) { - if (dest == null) - { + if (!dest) return null; - } - var result = dest; - if (src == null) - { + if (!src) return dest; - } - uint counter = 0; - byte* search = src; - while (*search != '\0') - { - *dest = *search; - search++; - dest++; - counter++; - if (counter == count) - { - break; - } - } + src.CopyTo(dest, count); - return result; + return dest; } public static byte* StrCat(byte* dest, byte* src) { @@ -184,24 +135,12 @@ public static int StrNCmp(byte* lhs, byte* rhs, nuint count) return dest; } - public static byte* StrChr(byte* str, int ch) + public static byte* StrChr(UTF8String str, int ch) { - if (str == null) - { + if (!str) return null; - } - - while (*str != 0) - { - if (*str == ch) - { - return str; - } - - str++; - } - return null; + return str.FindEntry((byte)ch); } public static int StrCmp(byte* lhs, byte* rhs) diff --git a/Cesium.Runtime/UTF8String.cs b/Cesium.Runtime/UTF8String.cs new file mode 100644 index 00000000..2285fe04 --- /dev/null +++ b/Cesium.Runtime/UTF8String.cs @@ -0,0 +1,141 @@ +using Cesium.Runtime.Attributes; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Cesium.Runtime; + +/// +/// A useful wrapper over UTF8 strings. +/// +[EquivalentType(typeof(byte*))] +[StructLayout(LayoutKind.Sequential)] +public unsafe readonly struct UTF8String +{ + public readonly static UTF8String NullString = new UTF8String((byte*)0); + + private readonly long _value; + + public UTF8String(byte* text) => _value = (long)text; + + public byte* Pointer => (byte*)_value; + + public byte this[int index] + { + get => Pointer[index]; + set => Pointer[index] = value; + } + + public byte this[nuint index] + { + get => Pointer[index]; + set => Pointer[index] = value; + } + + /// + /// String length + /// + public nuint Length + { + get + { + nuint length = 0; + while (Pointer[length] != 0) length++; + return length; + } + } + + /// + /// String length including '\0' + /// + public nuint NullTerminatedLength + { + get + { + nuint length = 0; + while (Pointer[length] != 0) length++; + return length + 1; + } + } + +#if !NETSTANDARD + /// + /// Creates a Span for the full length of the string + /// + public Span Span => new(Pointer, (int)Length); + + /// + /// Creates Span(ptr, int.MaxValue) + /// + public Span UncheckedSpan => new(Pointer, int.MaxValue); +#endif + + /// + /// Copies the contents of a string for its entire length to another string + /// + /// Destination + public void CopyTo(UTF8String dest) + { + var len = NullTerminatedLength; + +#if NETSTANDARD + for(nuint i = 0; i < len; i++) + dest[i] = this[i]; +#else + UncheckedSpan.Slice(0, (int)len).CopyTo(dest.UncheckedSpan); +#endif + } + + /// + /// Copies "count" bytes of a string to another string + /// + /// Destination + /// How many bytes to copy + public void CopyTo(UTF8String dest, nuint count) + { + var len = Math.Min(NullTerminatedLength, count); + +#if NETSTANDARD + for (nuint i = 0; i < len; i++) + dest[i] = this[i]; +#else + UncheckedSpan.Slice(0, (int)len).CopyTo(dest.UncheckedSpan); +#endif + } + + /// + /// Looks for a literal in a string + /// + /// ASCII literal + /// Pointer to the literal + public UTF8String FindEntry(byte ch) + { +#if NETSTANDARD + var len = Length; + for (nuint i = 0; i < len; i++) + if (this[i] == ch) + return At(i); + return NullString; +#else + var index = Span.IndexOf(ch); + if (index == -1) return NullString; + return (byte*)Unsafe.AsPointer(ref Unsafe.AddByteOffset(ref Unsafe.AsRef(Pointer), (nuint)index)); +#endif + } + + public UTF8String At(int index) => new UTF8String(Pointer + index); + public UTF8String At(nuint index) => new UTF8String(Pointer + index); + + public override string ToString() => new string((sbyte*)_value); + + public static bool operator !(UTF8String str) => (nuint)str._value == 0; + + public static implicit operator byte*(UTF8String str) => str.Pointer; + public static implicit operator IntPtr(UTF8String str) => (IntPtr)str._value; + + public static implicit operator UTF8String(byte* p) => new UTF8String(p); + public static implicit operator UTF8String(sbyte* p) => new UTF8String((byte*)p); + public static implicit operator UTF8String(IntPtr p) => new UTF8String((byte*)p); + public static implicit operator UTF8String(CPtr p) => new UTF8String(p.AsPtr()); + public static implicit operator UTF8String(CPtr p) => new UTF8String((byte*)p.AsPtr()); +} diff --git a/Cesium.Sdk.Tests/ArgumentUtilTests.cs b/Cesium.Sdk.Tests/ArgumentUtilTests.cs new file mode 100644 index 00000000..7df90d99 --- /dev/null +++ b/Cesium.Sdk.Tests/ArgumentUtilTests.cs @@ -0,0 +1,17 @@ +namespace Cesium.Sdk.Tests; + +public class ArgumentUtilTests +{ + [Fact] + public void PerformTests() + { + Assert.Equal("\"\"", ArgumentUtil.ToCommandLineString([""])); + Assert.Equal("a b c", ArgumentUtil.ToCommandLineString(["a", "b", "c"])); + Assert.Equal("\"a b\" c", ArgumentUtil.ToCommandLineString(["a b", "c"])); + Assert.Equal("a\\b c", ArgumentUtil.ToCommandLineString([@"a\b", "c"])); + Assert.Equal("\"\\\"\"", ArgumentUtil.ToCommandLineString(["\""])); + Assert.Equal("\"a \\\"b\\\"\"", ArgumentUtil.ToCommandLineString(["a \"b\""])); + Assert.Equal("\"\\\\\\\"\"", ArgumentUtil.ToCommandLineString(["\\\""])); + Assert.Equal("\"a\\ \\\\\\\"b\\\"\"", ArgumentUtil.ToCommandLineString(["a\\ \\\"b\""])); + } +} diff --git a/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj b/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj new file mode 100644 index 00000000..f4c8f60f --- /dev/null +++ b/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/Cesium.Sdk.Tests/CesiumCompileTests.cs b/Cesium.Sdk.Tests/CesiumCompileTests.cs new file mode 100644 index 00000000..e9156037 --- /dev/null +++ b/Cesium.Sdk.Tests/CesiumCompileTests.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using Cesium.TestFramework; +using Xunit.Abstractions; + +namespace Cesium.Sdk.Tests; + +public class CesiumCompileTests(ITestOutputHelper testOutputHelper) : SdkTestBase(testOutputHelper) +{ + [Theory] + [InlineData("SimpleCoreExe")] + public async Task CesiumCompile_Core_Exe_ShouldSucceed(string projectName) + { + HashSet expectedObjArtifacts = + [ + $"{projectName}.dll" + ]; + + var hostExeFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{projectName}.exe" : projectName; + HashSet expectedBinArtifacts = + [ + $"{projectName}.dll", + hostExeFile, + "Cesium.Runtime.dll", + $"{projectName}.runtimeconfig.json", + $"{projectName}.deps.json", + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertCollection.Includes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertCollection.Includes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } + + [Theory] + [InlineData("SimpleNetfxExe")] + public async Task CesiumCompile_NetFx_Exe_ShouldSucceed(string projectName) + { + HashSet expectedObjArtifacts = + [ + $"{projectName}.exe" + ]; + + HashSet expectedBinArtifacts = + [ + $"{projectName}.exe", + "Cesium.Runtime.dll", + $"{projectName}.runtimeconfig.json" + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertCollection.Includes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertCollection.Includes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } + + [Theory] + [InlineData("SimpleCoreLibrary")] + [InlineData("SimpleNetStandardLibrary")] + [InlineData("SimpleCoreLibraryWithHeader")] + public async Task CesiumCompile_Core_Library_ShouldSucceed(string projectName) + { + string[] expectedObjArtifacts = + [ + $"{projectName}.dll" + ]; + + string[] expectedBinArtifacts = + [ + $"{projectName}.dll", + $"{projectName}.deps.json", + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertCollection.Includes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertCollection.Includes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } + + [Theory] + [InlineData("SimpleNetfxLibrary")] + public async Task CesiumCompile_NetFxLibrary_ShouldSucceed(string projectName) + { + HashSet expectedObjArtifacts = + [ + $"{projectName}.dll" + ]; + + HashSet expectedBinArtifacts = + [ + $"{projectName}.dll", + "Cesium.Runtime.dll", + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertCollection.Includes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertCollection.Includes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } +} diff --git a/Cesium.Sdk.Tests/EvaluationTests.cs b/Cesium.Sdk.Tests/EvaluationTests.cs new file mode 100644 index 00000000..39ef319b --- /dev/null +++ b/Cesium.Sdk.Tests/EvaluationTests.cs @@ -0,0 +1,32 @@ +using Xunit.Abstractions; + +namespace Cesium.Sdk.Tests; + +/// +/// Tests that proper MSBuild items and properties are populated and set for the Cesium project +/// +/// +public class EvaluationTests(ITestOutputHelper testOutputHelper) : SdkTestBase(testOutputHelper) +{ + [Theory] + [InlineData("SimpleCoreLibraryWithHeader")] + public async Task Evaluation_EnableDefaultCompileItems(string projectName) + { + HashSet expectedCompileItems = ["library.c", "library.h"]; + + var items = await ListItems(projectName, "Compile"); + + Assert.Equal(expectedCompileItems, items.ToHashSet()); + } + + [Theory] + [InlineData("SimpleExplicitCompileItems")] + public async Task Evaluation_DisableDefaultCompileItems(string projectName) + { + HashSet expectedCompileItems = ["hello.c"]; + + var items = await ListItems(projectName, "Compile"); + + Assert.Equal(expectedCompileItems, items.ToHashSet()); + } +} diff --git a/Cesium.Sdk.Tests/SdkTestBase.cs b/Cesium.Sdk.Tests/SdkTestBase.cs new file mode 100644 index 00000000..d6a3989a --- /dev/null +++ b/Cesium.Sdk.Tests/SdkTestBase.cs @@ -0,0 +1,201 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Json; +using Cesium.Solution.Metadata; +using Cesium.TestFramework; +using Xunit.Abstractions; + +namespace Cesium.Sdk.Tests; + +public abstract class SdkTestBase : IDisposable +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _temporaryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly Dictionary _dotNetEnvVars; + + private string NuGetConfigPath => Path.Combine(_temporaryPath, "NuGet.config"); + private string GlobalJsonPath => Path.Combine(_temporaryPath, "global.json"); + + protected SdkTestBase(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _dotNetEnvVars = new() { ["NUGET_PACKAGES"] = Path.Combine(_temporaryPath, "package-cache") }; + + File.Delete(_temporaryPath); + + _testOutputHelper.WriteLine($"Test projects folder: {_temporaryPath}"); + + var assemblyPath = Assembly.GetExecutingAssembly().Location; + var testDataPath = Path.Combine(Path.GetDirectoryName(assemblyPath)!, "TestProjects"); + _testOutputHelper.WriteLine($"Copying TestProjects to {_temporaryPath}..."); + CopyDirectoryRecursive(testDataPath, _temporaryPath); + + var nupkgPath = Path.GetFullPath(Path.Combine(SolutionMetadata.SourceRoot, "artifacts", "package", "debug")); + _testOutputHelper.WriteLine($"Local NuGet feed: {nupkgPath}."); + EmitNuGetConfig(NuGetConfigPath, nupkgPath); + EmitGlobalJson(GlobalJsonPath, $"{SolutionMetadata.VersionPrefix}"); + } + + protected async Task ExecuteTargets(string projectName, params string[] targets) + { + var projectFile = $"{projectName}/{projectName}.ceproj"; + var joinedTargets = string.Join(";", targets); + var testProjectFile = Path.GetFullPath(Path.Combine(_temporaryPath, projectFile)); + var testProjectFolder = Path.GetDirectoryName(testProjectFile) ?? throw new ArgumentNullException(nameof(testProjectFile)); + var binLogFile = Path.Combine(testProjectFolder, $"build_result_{projectName}_{DateTime.UtcNow:yyyy-dd-M_HH-mm-s}.binlog"); + + const string objFolderPropertyName = "IntermediateOutputPath"; + const string binFolderPropertyName = "OutDir"; + + var startInfo = new ProcessStartInfo + { + WorkingDirectory = testProjectFolder, + FileName = "dotnet", + ArgumentList = { "msbuild", testProjectFile, $"/t:{joinedTargets}", "/restore", $"/bl:{binLogFile}" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false, + }; + foreach (var (name, var) in _dotNetEnvVars) + { + startInfo.Environment[name] = var; + } + + using var process = new Process(); + process.StartInfo = startInfo; + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + _testOutputHelper.WriteLine($"[stdout]: {e.Data}"); + } + }; + + process.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + _testOutputHelper.WriteLine($"[stderr]: {e.Data}"); + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + var success = process.ExitCode == 0; + + _testOutputHelper.WriteLine(success + ? "Build succeeded" + : $"Build failed with exit code {process.ExitCode}"); + + var properties = await DotNetCliHelper.EvaluateMSBuildProperties( + _testOutputHelper, + testProjectFile, + env: _dotNetEnvVars, + objFolderPropertyName, + binFolderPropertyName); + _testOutputHelper.WriteLine($"Properties request result: {JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = false })}"); + + var binFolder = NormalizePath(Path.GetFullPath(properties[binFolderPropertyName], testProjectFolder)); + var objFolder = NormalizePath(Path.GetFullPath(properties[objFolderPropertyName], testProjectFolder)); + + var binArtifacts = CollectArtifacts(binFolder); + var objArtifacts = CollectArtifacts(objFolder); + + var result = new BuildResult(process.ExitCode, binArtifacts, objArtifacts); + _testOutputHelper.WriteLine($"Build result: {JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true })}"); + return result; + + IReadOnlyCollection CollectArtifacts(string folder) + { + _testOutputHelper.WriteLine($"Collecting artifacts from '{folder}' folder"); + return Directory.Exists(folder) + ? Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) + .Select(path => new BuildArtifact(Path.GetRelativePath(folder, path), path)) + .ToList() + : Array.Empty(); + } + } + + protected async Task> ListItems(string projectName, string itemName) + { + var projectFile = $"{projectName}/{projectName}.ceproj"; + var testProjectFile = Path.GetFullPath(Path.Combine(_temporaryPath, projectFile)); + var items = await DotNetCliHelper.EvaluateMSBuildItem(_testOutputHelper, testProjectFile, itemName, env: _dotNetEnvVars); + + return items.Select(i => i.identity); + } + + private static void EmitNuGetConfig(string configFilePath, string packageSourcePath) + { + File.WriteAllText(configFilePath, $""" + + + + + + """); + } + + private static void EmitGlobalJson(string globalJsonPath, string packageVersion) + { + File.WriteAllText(globalJsonPath, $$""" + { + "msbuild-sdks": { + "Cesium.Sdk" : "{{packageVersion}}" + } + } + """); + } + + private static void CopyDirectoryRecursive(string source, string target) + { + Directory.CreateDirectory(target); + + foreach (var subDirPath in Directory.GetDirectories(source)) + { + var dirName = Path.GetFileName(subDirPath); + CopyDirectoryRecursive(subDirPath, Path.Combine(target, dirName)); + } + + foreach (var filePath in Directory.GetFiles(source)) + { + var fileName = Path.GetFileName(filePath); + File.Copy(filePath, Path.Combine(target, fileName)); + } + } + + private static string NormalizePath(string path) + { + var normalizedPath = new Uri(path).LocalPath; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? normalizedPath + : normalizedPath.Replace('\\', '/'); + } + + protected record BuildResult( + int ExitCode, + IReadOnlyCollection OutputArtifacts, + IReadOnlyCollection IntermediateArtifacts); + + protected record BuildArtifact( + string FileName, + string FullPath); + + private void ClearOutput() + { + Directory.Delete(_temporaryPath, true); + } + + public void Dispose() + { + ClearOutput(); + } +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/SimpleCoreExe.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/SimpleCoreExe.ceproj new file mode 100644 index 00000000..b359b970 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/SimpleCoreExe.ceproj @@ -0,0 +1,6 @@ + + + net6.0 + Exe + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/hello.c b/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/hello.c new file mode 100644 index 00000000..2889672e --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreExe/hello.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char *argv[]) +{ + puts("Hello, world!"); + return 42; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/SimpleCoreLibrary.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/SimpleCoreLibrary.ceproj new file mode 100644 index 00000000..fa9b0e83 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/SimpleCoreLibrary.ceproj @@ -0,0 +1,6 @@ + + + net6.0 + Library + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/library.c b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/library.c new file mode 100644 index 00000000..c6d65a06 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibrary/library.c @@ -0,0 +1,29 @@ +#include + +void greet() +{ + puts("Hello World!\n"); +} + +int add(int a, int b) +{ + return a + b; +} + +int subtract(int a, int b) +{ + return a - b; +} + +float divide(int a, int b) +{ + if (b != 0) + return (float) a / b; + else + return 0; +} + +int multiply(int a, int b) +{ + return a * b; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/SimpleCoreLibraryWithHeader.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/SimpleCoreLibraryWithHeader.ceproj new file mode 100644 index 00000000..fa9b0e83 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/SimpleCoreLibraryWithHeader.ceproj @@ -0,0 +1,6 @@ + + + net6.0 + Library + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.c b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.c new file mode 100644 index 00000000..c1075b29 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.c @@ -0,0 +1,30 @@ +#include "library.h" +#include + +void greet() +{ + puts("Hello World!\n"); +} + +int add(int a, int b) +{ + return a + b; +} + +int subtract(int a, int b) +{ + return a - b; +} + +float divide(int a, int b) +{ + if (b != 0) + return (float) a / b; + else + return 0; +} + +int multiply(int a, int b) +{ + return a * b; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.h b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.h new file mode 100644 index 00000000..9651f0f8 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleCoreLibraryWithHeader/library.h @@ -0,0 +1,10 @@ +#ifndef LIBRARY_H +#define LIBRARY_H + +void greet(void); +int add(int a, int b); +int subtract(int a, int b); +float divide(int a, int b); +int multiply(int a, int b); + +#endif // LIBRARY_H diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/SimpleExplicitCompileItems.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/SimpleExplicitCompileItems.ceproj new file mode 100644 index 00000000..d9380112 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/SimpleExplicitCompileItems.ceproj @@ -0,0 +1,10 @@ + + + net6.0 + Exe + false + + + + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/hello.c b/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/hello.c new file mode 100644 index 00000000..2889672e --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleExplicitCompileItems/hello.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char *argv[]) +{ + puts("Hello, world!"); + return 42; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/SimpleNetStandardLibrary.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/SimpleNetStandardLibrary.ceproj new file mode 100644 index 00000000..d034cadc --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/SimpleNetStandardLibrary.ceproj @@ -0,0 +1,6 @@ + + + netstandard2.0 + Library + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/library.c b/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/library.c new file mode 100644 index 00000000..c6d65a06 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetStandardLibrary/library.c @@ -0,0 +1,29 @@ +#include + +void greet() +{ + puts("Hello World!\n"); +} + +int add(int a, int b) +{ + return a + b; +} + +int subtract(int a, int b) +{ + return a - b; +} + +float divide(int a, int b) +{ + if (b != 0) + return (float) a / b; + else + return 0; +} + +int multiply(int a, int b) +{ + return a * b; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/SimpleNetfxExe.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/SimpleNetfxExe.ceproj new file mode 100644 index 00000000..06eaf71d --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/SimpleNetfxExe.ceproj @@ -0,0 +1,6 @@ + + + net48 + Exe + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/hello.c b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/hello.c new file mode 100644 index 00000000..2889672e --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxExe/hello.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char *argv[]) +{ + puts("Hello, world!"); + return 42; +} diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/SimpleNetfxLibrary.ceproj b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/SimpleNetfxLibrary.ceproj new file mode 100644 index 00000000..324bb982 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/SimpleNetfxLibrary.ceproj @@ -0,0 +1,6 @@ + + + net48 + Library + + diff --git a/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/library.c b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/library.c new file mode 100644 index 00000000..c6d65a06 --- /dev/null +++ b/Cesium.Sdk.Tests/TestProjects/SimpleNetfxLibrary/library.c @@ -0,0 +1,29 @@ +#include + +void greet() +{ + puts("Hello World!\n"); +} + +int add(int a, int b) +{ + return a + b; +} + +int subtract(int a, int b) +{ + return a - b; +} + +float divide(int a, int b) +{ + if (b != 0) + return (float) a / b; + else + return 0; +} + +int multiply(int a, int b) +{ + return a * b; +} diff --git a/Cesium.Sdk.Tests/Usings.cs b/Cesium.Sdk.Tests/Usings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/Cesium.Sdk.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/Cesium.Sdk/ArgumentUtil.cs b/Cesium.Sdk/ArgumentUtil.cs new file mode 100644 index 00000000..8df5dd0c --- /dev/null +++ b/Cesium.Sdk/ArgumentUtil.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Cesium.Sdk; + +public static class ArgumentUtil +{ + public static string ToCommandLineString(IEnumerable args) + { + var result = new StringBuilder(); + var first = true; + foreach (var a in args) + { + if (first) + { + first = false; + } + else + { + result.Append(' '); + } + if (a.Length == 0 || a.Any(c => char.IsWhiteSpace(c) || c == '"')) + { + result.Append(Quoted(a)); + } + else + { + result.Append(a); + } + } + return result.ToString(); + } + + private static string Quoted(string arg) + { + // The simplified rules: + // 1. Every quote should be escaped by \ + // 2. Slashes preceding the quotes should be escaped by \ + // + // Meaning any amount of slashes following a quote should be converted to twice as many slashes + slash + quote. + // The final quote (not part of the argument per se) also counts as quote for this purpose. + var result = new StringBuilder(arg.Length + 2); + result.Append('"'); + var slashes = 0; + foreach (var c in arg) + { + switch (c) + { + case '\\': + slashes++; + break; + case '"': + result.Append('\\', slashes * 2); + slashes = 0; + + result.Append("\\\""); + break; + default: + result.Append('\\', slashes); + slashes = 0; + + result.Append(c); + break; + } + } + + result.Append('\\', slashes * 2); + result.Append('"'); + + return result.ToString(); + } +} diff --git a/Cesium.Sdk/Cesium.Sdk.csproj b/Cesium.Sdk/Cesium.Sdk.csproj new file mode 100644 index 00000000..4e8d3843 --- /dev/null +++ b/Cesium.Sdk/Cesium.Sdk.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + true + + + + + + + + + + + + + + + diff --git a/Cesium.Sdk/CesiumCompile.cs b/Cesium.Sdk/CesiumCompile.cs new file mode 100644 index 00000000..cc756379 --- /dev/null +++ b/Cesium.Sdk/CesiumCompile.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Cesium.Sdk; + +/* + -o, --out Sets path for the output assembly file + --framework (Default: Net) Valid values: Net, NetFramework, NetStandard + --arch (Default: Dynamic) Valid values: Dynamic, Bit32, Bit64 + --modulekind Valid values: Dll, Console, Windows, NetModule + --nologo Suppress compiler banner message + --namespace Sets default namespace instead of "global" + --globalclass Sets default global class instead of "" + --import Provides path to assemblies which would be added as references automatically into resulting executable. + --corelib Sets path to CoreLib assembly + --runtime Sets path to Cesium C Runtime assembly + -O Set the optimization level + -W Enable warnings set + -D Define constants for preprocessor + --help Display this help screen. + --version Display version information. + value pos. 0 + */ + +// ReSharper disable once UnusedType.Global +public class CesiumCompile : Task +{ + [Required] public string CompilerExe { get; set; } = null!; + [Required] public ITaskItem[] InputFiles { get; set; } = null!; + [Required] public string OutputFile { get; set; } = null!; + + public string? Namespace { get; set; } + public string? Framework { get; set; } + public string? Architecture { get; set; } + public string? ModuleType { get; set; } + public string? CoreLibPath { get; set; } + public string? RuntimePath { get; set; } + public ITaskItem[] ImportItems { get; set; } = Array.Empty(); + public ITaskItem[] PreprocessorItems { get; set; } = Array.Empty(); + public bool DryRun = false; + + [Output] public string? ResultingCommandLine { get; private set; } + [Output] public TaskItem[]? OutputFiles { get; private set; } + + public override bool Execute() + { + if (!TryValidate(out var options) || options is null) + { + return false; + } + + var compilerProcess = new Process + { + StartInfo = + { + FileName = options.CompilerExe, + Arguments = CollectCommandLineArguments(options), + UseShellExecute = false, + } + }; + + ResultingCommandLine = $"{compilerProcess.StartInfo.FileName} {compilerProcess.StartInfo.Arguments}"; + OutputFiles = [new TaskItem(OutputFile)]; + + if (!DryRun) + { + compilerProcess.Start(); + compilerProcess.WaitForExit(); + } + + return true; + } + + private static (bool, FrameworkKind?) TryParseFramework(string? framework) => framework switch + { + null => (true, null), + nameof(FrameworkKind.Net) => (true, FrameworkKind.Net), + nameof(FrameworkKind.NetFramework) => (true, FrameworkKind.NetFramework), + nameof(FrameworkKind.NetStandard) => (true, FrameworkKind.NetStandard), + _ => (false, null) + }; + + private static (bool, ArchitectureKind?) TryParseArchitectureKind(string? archKind) => archKind switch + { + null => (true, null), + nameof(ArchitectureKind.Dynamic) => (true, ArchitectureKind.Dynamic), + nameof(ArchitectureKind.Bit32) => (true, ArchitectureKind.Bit32), + nameof(ArchitectureKind.Bit64) => (true, ArchitectureKind.Bit64), + _ => (false, null) + }; + + private static (bool, ModuleKind?) TryParseModuleKind(string? moduleKind) => moduleKind switch + { + null => (true, null), + nameof(ModuleKind.Dll) => (true, ModuleKind.Dll), + nameof(ModuleKind.Console) => (true, ModuleKind.Console), + nameof(ModuleKind.Windows) => (true, ModuleKind.Windows), + nameof(ModuleKind.NetModule) => (true, ModuleKind.NetModule), + _ => (false, null) + }; + + private bool TryValidate(out ValidatedOptions? options) + { + options = null; + var success = true; + + + if (!string.IsNullOrWhiteSpace(CompilerExe) && !File.Exists(CompilerExe)) + { + ReportValidationError("CES1000", $"Compiler executable doesn't exist under path '{CompilerExe}'"); + success = false; + } + + var (isFrameworkValid, framework) = TryParseFramework(Framework); + if (!isFrameworkValid) + { + var validValues = Enum.GetValues(typeof(FrameworkKind)).Cast().Select(kind => kind.ToString()); + ReportValidationError("CES1004", $"Framework should be in range: '{string.Join(", ", validValues)}', actual: '{Framework}'"); + success = false; + } + + var (isArchValid, arch) = TryParseArchitectureKind(Architecture); + if (!isArchValid) + { + var validValues = Enum.GetValues(typeof(ArchitectureKind)).Cast().Select(kind => kind.ToString()); + ReportValidationError("CES1005", $"Architecture should be in range: '{string.Join(", ", validValues)}', actual: '{Architecture}'"); + success = false; + } + + var (isModuleKindValid, moduleKind) = TryParseModuleKind(ModuleType); + if (!isModuleKindValid) + { + var validValues = Enum.GetValues(typeof(ModuleKind)).Cast().Select(kind => kind.ToString()); + ReportValidationError("CES1006", $"ModuleKind should be in range: '{string.Join(", ", validValues)}', actual: '{ModuleType}'"); + success = false; + } + + var missingCompileItems = InputFiles.Where(item => !File.Exists(item.ItemSpec)).ToList(); + foreach (var item in missingCompileItems) + { + ReportValidationError("CES1001", $"Source file doesn't exist: '{item.ItemSpec}'"); + success = false; + } + + if (!string.IsNullOrWhiteSpace(CoreLibPath) && !File.Exists(CoreLibPath)) + { + ReportValidationError("CES1002", $"CorLib doesn't exist under path '{CoreLibPath}'"); + success = false; + } + + if (!string.IsNullOrWhiteSpace(RuntimePath) && !File.Exists(RuntimePath)) + { + ReportValidationError("CES1003", $"Cesium.Runtime doesn't exist under path '{RuntimePath}'"); + success = false; + } + + var missingAssemblyImportItems = ImportItems.Where(item => !File.Exists(item.ItemSpec)).ToList(); + foreach (var item in missingAssemblyImportItems) + { + ReportValidationError("CES1001", $"Imported assembly doesn't exist: '{item.ItemSpec}'"); + success = false; + } + + if (!success) return false; + + options = new ValidatedOptions( + CompilerExe: CompilerExe, + InputItems: InputFiles.Select(item => item.ItemSpec).ToArray(), + OutputFile: OutputFile, + Namespace: Namespace, + Framework: framework, + Architecture: arch, + ModuleKind: moduleKind, + CoreLibPath: CoreLibPath, + RuntimePath: RuntimePath, + ImportItems: ImportItems.Select(item => item.ItemSpec).ToArray(), + PreprocessorItems: PreprocessorItems.Select(item => item.ItemSpec).ToArray() + ); + + return true; + } + + private string CollectCommandLineArguments(ValidatedOptions options) + { + var args = new List(); + + args.Add("--nologo"); + + if (options.Framework is { } framework) + { + args.Add("--framework"); + args.Add(framework.ToString()); + } + + if (options.Architecture is { } arch) + { + args.Add("--arch"); + args.Add(arch.ToString()); + } + + if (options.ModuleKind is { } moduleKind) + { + args.Add("--modulekind"); + args.Add(moduleKind.ToString()); + } + + if (!string.IsNullOrWhiteSpace(options.Namespace)) + { + args.Add("--namespace"); + args.Add(options.Namespace!); + } + + foreach (var import in options.ImportItems) + { + args.Add("--import"); + args.Add(import); + } + + if (!string.IsNullOrWhiteSpace(options.CoreLibPath)) + { + args.Add("--corelib"); + args.Add(options.CoreLibPath!); + } + + if (!string.IsNullOrWhiteSpace(options.RuntimePath)) + { + args.Add("--runtime"); + args.Add(options.RuntimePath!); + } + + foreach (var item in options.PreprocessorItems) + { + args.Add("-D"); + args.Add(item); + } + + args.Add("--out"); + args.Add(options.OutputFile); + + foreach (var input in options.InputItems) + { + args.Add(input); + } + + return ArgumentUtil.ToCommandLineString(args); + } + + private void ReportValidationError(string code, string message) => + BuildEngine.LogErrorEvent(new BuildErrorEventArgs(nameof(CesiumCompile), code, string.Empty, -1, -1, -1, -1, message, string.Empty, nameof(CesiumCompile))); + + private void ReportValidationWarning(string code, string message) => + BuildEngine.LogWarningEvent(new BuildWarningEventArgs(nameof(CesiumCompile), code, string.Empty, -1, -1, -1, -1, message, string.Empty, nameof(CesiumCompile))); + + private string GetResultingCommandLine(string executable, IReadOnlyCollection arguments) + { + return $"{executable} {string.Join(" ", arguments)}"; + } + + private enum FrameworkKind + { + Net, + NetFramework, + NetStandard + } + + private enum ArchitectureKind + { + Dynamic, + Bit32, + Bit64 + } + + private enum ModuleKind + { + Dll, + Console, + Windows, + NetModule + } + + private record ValidatedOptions( + string CompilerExe, + string[] InputItems, + string OutputFile, + string? Namespace, + FrameworkKind? Framework, + ArchitectureKind? Architecture, + ModuleKind? ModuleKind, + string? CoreLibPath, + string? RuntimePath, + string[] ImportItems, + string[] PreprocessorItems + ); +} diff --git a/Cesium.Sdk/IsExternalInit.cs b/Cesium.Sdk/IsExternalInit.cs new file mode 100644 index 00000000..eb2da113 --- /dev/null +++ b/Cesium.Sdk/IsExternalInit.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit {} +} diff --git a/Cesium.Sdk/Sdk/Sdk.props b/Cesium.Sdk/Sdk/Sdk.props new file mode 100644 index 00000000..9c1afd11 --- /dev/null +++ b/Cesium.Sdk/Sdk/Sdk.props @@ -0,0 +1,45 @@ + + + + true + true + false + false + false + false + .c + + + + + + false + false + Cesium.Compiler.Bundle.$(NETCoreSdkRuntimeIdentifier) + + 0.0.1 + false + + + + + + + + + + + + + + + + + + diff --git a/Cesium.Sdk/Sdk/Sdk.targets b/Cesium.Sdk/Sdk/Sdk.targets new file mode 100644 index 00000000..c59669a6 --- /dev/null +++ b/Cesium.Sdk/Sdk/Sdk.targets @@ -0,0 +1,112 @@ + + + + + + true + false + + + + <_CesiumCompilerPackageFolderName Condition="$(CesiumCompilerPackagePath) == ''">$(CesiumCompilerPackageName.ToLower()) + <_CesiumCompilerPackagePath>$([System.IO.Path]::Combine($(NuGetPackageRoot), $(_CesiumCompilerPackageFolderName), $(CesiumCompilerPackageVersion))) + $(_CesiumCompilerPackagePath)/tools/Cesium.Compiler + $(_CesiumCompilerPackagePath)\tools\Cesium.Compiler.exe + + + + + + + + + + + + <_CoreLibAssembly Include="@(ReferencePath)" Condition="'%(ReferencePath.AssemblyName)'=='System.Runtime' OR '%(ReferencePath.AssemblyName)'=='mscorlib'" /> + + + %(_CoreLibAssembly.Identity) + + + + + <_CesiumModuleKind Condition="'$(OutputType)' == 'Exe'">Console + <_CesiumModuleKind Condition="'$(OutputType)' == 'WinExe'">Windows + <_CesiumModuleKind Condition="'$(OutputType)' == 'Library'">Dll + + + + + + + + <_CesiumFramework Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(TargetFrameworkVersion)' == 'v6.0'">Net + <_CesiumFramework Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND '$(TargetFrameworkVersion)' == 'v4.8'">NetFramework + <_CesiumFramework Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard' AND '$(TargetFrameworkVersion)' == 'v2.0'">NetStandard + + + + + + + + <_CesiumArchitecture Condition="'$(Platform)' == 'AnyCPU'">Dynamic + <_CesiumArchitecture Condition="'$(Platform)' == 'x64'">Bit64 + <_CesiumArchitecture Condition="'$(Platform)' == 'x86'">Bit32 + + + + + + + + <_CompilerOutputBase>$(IntermediateOutputPath)$(AssemblyName) + <_CompilerOutputExtenion>dll + + <_CompilerOutputExtenion Condition="$(_CesiumFramework) == 'NetFramework' AND $(OutputType) != 'Library'">exe + <_CompilerOutput>$(_CompilerOutputBase).$(_CompilerOutputExtenion) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cesium.Sdk/TestTargets.targets b/Cesium.Sdk/TestTargets.targets new file mode 100644 index 00000000..fcddc5d8 --- /dev/null +++ b/Cesium.Sdk/TestTargets.targets @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Cesium.Solution.Metadata/Cesium.Solution.Metadata.csproj b/Cesium.Solution.Metadata/Cesium.Solution.Metadata.csproj new file mode 100644 index 00000000..099620c0 --- /dev/null +++ b/Cesium.Solution.Metadata/Cesium.Solution.Metadata.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + false + + + + + <_Parameter1>$(CesiumSourceRoot) + <_Parameter2>$(ArtifactsPath) + <_Parameter3>$(VersionPrefix) + + + + diff --git a/Cesium.Solution.Metadata/SolutionMetadata.cs b/Cesium.Solution.Metadata/SolutionMetadata.cs new file mode 100644 index 00000000..761c29a8 --- /dev/null +++ b/Cesium.Solution.Metadata/SolutionMetadata.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace Cesium.Solution.Metadata; + +public static class SolutionMetadata +{ + public static string SourceRoot => ResolvedAttribute.SourceRoot; + public static string ArtifactsRoot => ResolvedAttribute.ArtifactsRoot; + public static string VersionPrefix => ResolvedAttribute.VersionPrefix; + + private static SolutionMetadataAttribute ResolvedAttribute => + Assembly.GetExecutingAssembly().GetCustomAttribute() + ?? throw new Exception($"Missing {nameof(SolutionMetadataAttribute)} metadata attribute."); +} diff --git a/Cesium.Solution.Metadata/SolutionMetadataAttribute.cs b/Cesium.Solution.Metadata/SolutionMetadataAttribute.cs new file mode 100644 index 00000000..ecf3e7eb --- /dev/null +++ b/Cesium.Solution.Metadata/SolutionMetadataAttribute.cs @@ -0,0 +1,16 @@ +namespace Cesium.Solution.Metadata; + +/// This attribute is only used by the Cesium test infrastructure. +public class SolutionMetadataAttribute : Attribute +{ + public string SourceRoot { get; } + public string ArtifactsRoot { get; } + public string VersionPrefix { get; } + + public SolutionMetadataAttribute(string sourceRoot, string artifactsRoot, string versionPrefix) + { + SourceRoot = sourceRoot; + ArtifactsRoot = artifactsRoot; + VersionPrefix = versionPrefix; + } +} diff --git a/Cesium.Templates.CSharp/Cesium.Templates.CSharp.csproj b/Cesium.Templates.CSharp/Cesium.Templates.CSharp.csproj new file mode 100644 index 00000000..803aa64f --- /dev/null +++ b/Cesium.Templates.CSharp/Cesium.Templates.CSharp.csproj @@ -0,0 +1,19 @@ + + + Cesium .NET Templates + Templates to use when creating a Cesium applications or libraries. + Template + netstandard2.0 + true + false + content + + $(NoWarn);NU5128 + Cesium.Templates.CSharp + + + + + + + diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/.template.config/template.json b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/.template.config/template.json new file mode 100644 index 00000000..d0241951 --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/.template.config/template.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Cesium Team", + "classifications": [ "Console" ], + "identity": "Cesium.Template.App.CSharp", + "name": "Cesium Console Application", + "shortName": "cesiumapp", + "tags": { + "language": "C", + "type":"project" + }, + "sourceName": "MyProject.Name", + "preferNameDirectory": true, + "symbols": { + "CesiumVersion": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "0.0.1" + }, + "replaces": "CesiumVersion" + }, + "tfm": { + "type": "parameter", + "datatype": "choice", + "defaultValue": "net6.0", + "choices": [ + { + "choice": "net6.0", + "description": ".NET 6.0 target framework" + }, + { + "choice": "net48", + "description": ".NET Framework 4.8 target framework" + } + ], + "replaces":"Tfm" + } + } +} diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/MyProject.Name.ceproj b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/MyProject.Name.ceproj new file mode 100644 index 00000000..20a785f2 --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/MyProject.Name.ceproj @@ -0,0 +1,9 @@ + + + Tfm + Exe + + + + + diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/program.c b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/program.c new file mode 100644 index 00000000..2889672e --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.App.CSharp/program.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char *argv[]) +{ + puts("Hello, world!"); + return 42; +} diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/.template.config/template.json b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/.template.config/template.json new file mode 100644 index 00000000..0cb9d45d --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/.template.config/template.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Cesium Team", + "classifications": [ "Console" ], + "identity": "Cesium.Template.Library.CSharp", + "name": "Cesium Library", + "shortName": "cesiumlib", + "tags": { + "language": "C", + "type":"project" + }, + "sourceName": "MyProject.Name", + "preferNameDirectory": true, + "symbols": { + "CesiumVersion": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "0.0.1" + }, + "replaces": "CesiumVersion" + }, + "tfm": { + "type": "parameter", + "datatype": "choice", + "defaultValue": "net6.0", + "choices": [ + { + "choice": "net6.0", + "description": ".NET 6.0 target framework" + }, + { + "choice": "netstandard2.0", + "description": ".NET Standard 2.0 target framework" + }, + { + "choice": "net48", + "description": ".NET Framework 4.8 target framework" + } + ], + "replaces":"Tfm" + } + } +} diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/MyProject.Name.ceproj b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/MyProject.Name.ceproj new file mode 100644 index 00000000..989b06b8 --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/MyProject.Name.ceproj @@ -0,0 +1,9 @@ + + + Tfm + Library + + + + + diff --git a/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/lib.c b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/lib.c new file mode 100644 index 00000000..be1e084f --- /dev/null +++ b/Cesium.Templates.CSharp/content/Cesium.Template.Library.CSharp/lib.c @@ -0,0 +1,4 @@ +int add(int a, int b) +{ + return a + b; +} diff --git a/Cesium.TestFramework.Tests/Cesium.TestFramework.Tests.csproj b/Cesium.TestFramework.Tests/Cesium.TestFramework.Tests.csproj index 33440b6f..f4dd9c6b 100644 --- a/Cesium.TestFramework.Tests/Cesium.TestFramework.Tests.csproj +++ b/Cesium.TestFramework.Tests/Cesium.TestFramework.Tests.csproj @@ -16,10 +16,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Cesium.TestFramework/AssertCollection.cs b/Cesium.TestFramework/AssertCollection.cs new file mode 100644 index 00000000..ab29b7fd --- /dev/null +++ b/Cesium.TestFramework/AssertCollection.cs @@ -0,0 +1,14 @@ +using Cesium.TestFramework.Exceptions; + +namespace Cesium.TestFramework; + +public static class AssertCollection +{ + public static void Includes(IReadOnlyCollection expected, IReadOnlyCollection all) + { + var foundItems = all.Where(expected.Contains).ToList(); + var remainingItems = expected.Except(foundItems).ToList(); + if (remainingItems.Count != 0) + throw new IncludesAssertFailedException(remainingItems); + } +} diff --git a/Cesium.TestFramework/CSharpCompilationUtil.cs b/Cesium.TestFramework/CSharpCompilationUtil.cs index 7a08a7f8..3775f015 100644 --- a/Cesium.TestFramework/CSharpCompilationUtil.cs +++ b/Cesium.TestFramework/CSharpCompilationUtil.cs @@ -1,6 +1,8 @@ using System.Xml.Linq; using System.Xml.XPath; +using AsyncKeyedLock; using Cesium.CodeGen; +using Cesium.Solution.Metadata; using Xunit.Abstractions; namespace Cesium.TestFramework; @@ -15,7 +17,7 @@ public static class CSharpCompilationUtil private const string _projectName = "TestProject"; /// Semaphore that controls the amount of simultaneously running tests. - private static readonly SemaphoreSlim _testSemaphore = new(Environment.ProcessorCount); + private static readonly AsyncNonKeyedLocker _testSemaphore = new(Environment.ProcessorCount); public static async Task CompileCSharpAssembly( ITestOutputHelper output, @@ -23,8 +25,7 @@ public static async Task CompileCSharpAssembly( string cSharpSource) { if (runtime != DefaultRuntime) throw new Exception($"Runtime {runtime} not supported for test compilation."); - await _testSemaphore.WaitAsync(); - try + using (await _testSemaphore.LockAsync()) { var directory = Path.GetTempFileName(); File.Delete(directory); @@ -35,10 +36,6 @@ public static async Task CompileCSharpAssembly( await CompileCSharpProject(output, directory, _projectName); return Path.Combine(projectDirectory, "bin", _configuration, _targetRuntime, _projectName + ".dll"); } - finally - { - _testSemaphore.Release(); - } } private static async Task CreateCSharpProject(ITestOutputHelper output, string directory) @@ -70,11 +67,10 @@ await ExecUtil.RunToSuccess( } public static readonly string CesiumRuntimeLibraryPath = Path.Combine( - TestStructureUtil.SolutionRootPath, - "Cesium.Runtime", + SolutionMetadata.ArtifactsRoot, "bin", - _configuration, - _cesiumRuntimeLibTargetRuntime, + "Cesium.Runtime", + $"{_configuration.ToLower()}_{_cesiumRuntimeLibTargetRuntime}", "Cesium.Runtime.dll"); private static Task CompileCSharpProject(ITestOutputHelper output, string directory, string projectName) => diff --git a/Cesium.TestFramework/Cesium.TestFramework.csproj b/Cesium.TestFramework/Cesium.TestFramework.csproj index d4c1bed7..816fed19 100644 --- a/Cesium.TestFramework/Cesium.TestFramework.csproj +++ b/Cesium.TestFramework/Cesium.TestFramework.csproj @@ -6,8 +6,11 @@ - + + + + @@ -16,6 +19,7 @@ + diff --git a/Cesium.TestFramework/DotNetCliHelper.cs b/Cesium.TestFramework/DotNetCliHelper.cs index d945fda6..21d32032 100644 --- a/Cesium.TestFramework/DotNetCliHelper.cs +++ b/Cesium.TestFramework/DotNetCliHelper.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Medallion.Shell; using Xunit.Abstractions; @@ -25,6 +26,58 @@ await RunToSuccess(output, "dotnet", Path.GetDirectoryName(projectFilePath)!, ne }); } + public static async Task EvaluateMSBuildProperty(ITestOutputHelper output, string projectPath, string propertyName) + { + var result = await ExecUtil.Run(output, "dotnet", Environment.CurrentDirectory, [ "msbuild", $"\"{projectPath}\"", $"-getProperty:{propertyName}" ]); + return result.StandardOutput; + } + + public static async Task> EvaluateMSBuildProperties( + ITestOutputHelper output, + string projectPath, + IReadOnlyDictionary? env = null, + params string[] propertyNames) + { + if (!propertyNames.Any()) + return new Dictionary(); + + var result = await ExecUtil.Run( + output, + "dotnet", + Environment.CurrentDirectory, + [ "msbuild", $"\"{projectPath}\"", $"-getProperty:{string.Join(",", propertyNames)}" ], + env); + var resultString = result.StandardOutput; + if (propertyNames.Length == 1) + return new Dictionary { { propertyNames[0], resultString } }; + + var resultJson = JsonDocument.Parse(resultString); + var propertiesJson = resultJson.RootElement.GetProperty("Properties").EnumerateObject().ToArray(); + + return propertiesJson + .ToDictionary(property => property.Name, property => property.Value.GetString() ?? string.Empty); + } + + public static async Task> EvaluateMSBuildItem( + ITestOutputHelper output, + string projectPath, + string itemName, + IReadOnlyDictionary? env = null) + { + var result = await ExecUtil.Run( + output, + "dotnet", + Environment.CurrentDirectory, + [ "msbuild", $"\"{projectPath}\"", $"-getItem:{itemName}" ], + env); + var resultString = result.StandardOutput; + var resultJson = JsonDocument.Parse(resultString); + var itemsJson = resultJson.RootElement.GetProperty("Items").EnumerateObject().ToArray(); + var itemsDict = itemsJson.ToDictionary(item => item.Name, item => item.Value.EnumerateArray()); + + return itemsDict[itemName].Select(meta => (meta.GetProperty("Identity").GetString()!, meta.GetProperty("FullPath").GetString())); + } + public static Task RunDotNetDll( ITestOutputHelper output, string workingDirectoryPath, diff --git a/Cesium.TestFramework/Exceptions/IncludesAssertFailedException.cs b/Cesium.TestFramework/Exceptions/IncludesAssertFailedException.cs new file mode 100644 index 00000000..48a8d4dd --- /dev/null +++ b/Cesium.TestFramework/Exceptions/IncludesAssertFailedException.cs @@ -0,0 +1,8 @@ +using Xunit.Sdk; + +namespace Cesium.TestFramework.Exceptions; + +public class IncludesAssertFailedException( + IEnumerable expected, + Exception? innerException = null) + : XunitException($"Expected elements are missing: [{string.Join(", ", expected)}]", innerException); diff --git a/Cesium.TestFramework/TestFileVerification.cs b/Cesium.TestFramework/TestFileVerification.cs index e04dc8f8..8aa8977d 100644 --- a/Cesium.TestFramework/TestFileVerification.cs +++ b/Cesium.TestFramework/TestFileVerification.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text; +using Cesium.Solution.Metadata; namespace Cesium.TestFramework; @@ -29,7 +30,7 @@ List Relativize(IEnumerable paths) => public static void VerifyAllTestsFromAssembly(Assembly assembly) { - var testClasses = assembly.GetTypes().Where(t => t.GetCustomAttributes().Any()).ToList(); + var testClasses = assembly.GetTypes().Where(t => t.IsAssignableTo(typeof(VerifyTestBase))).ToList(); Verify(testClasses); } @@ -49,16 +50,19 @@ private static Assembly GetTestAssembly(IEnumerable types) private static string GetTestProjectSourceDirectory(Assembly assembly) { - var currentPath = Path.GetDirectoryName(assembly.Location)!; - while (!Directory.EnumerateFileSystemEntries(currentPath, "*.csproj").Any()) - { - currentPath = Path.GetDirectoryName(currentPath); - if (currentPath == null) - throw new InvalidOperationException( - $"Could not find the test project source directory for assembly \"{assembly.Location}\"."); - } - - return currentPath; + // Assuming that output assembly name (AssemblyName MSBuild property) is equal to the project name + var projectName = assembly.GetName().Name; + if (projectName is null) + throw new InvalidOperationException( + $"Name is missing for an assembly at location \"{assembly.Location}\"."); + + var fullProjectFolderPath = Path.Combine(SolutionMetadata.SourceRoot, projectName); + var fullProjectFilePath = Path.Combine(fullProjectFolderPath, $"{projectName}.csproj"); + if (!File.Exists(fullProjectFilePath)) + throw new InvalidOperationException( + $"Could not find the test project source directory for assembly \"{assembly.Location}\"."); + + return fullProjectFolderPath; } private static IReadOnlySet GetAcceptedFilePaths(string sourceDirectory) diff --git a/Cesium.TestFramework/TestStructureUtil.cs b/Cesium.TestFramework/TestStructureUtil.cs deleted file mode 100644 index 0051d8df..00000000 --- a/Cesium.TestFramework/TestStructureUtil.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; - -namespace Cesium.TestFramework; - -public static class TestStructureUtil -{ - public static readonly string SolutionRootPath = GetSolutionRoot(); - - private static string GetSolutionRoot() - { - var assemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var currentDirectory = assemblyDirectory; - while (currentDirectory != null) - { - if (File.Exists(Path.Combine(currentDirectory, "Cesium.sln"))) - return currentDirectory; - - currentDirectory = Path.GetDirectoryName(currentDirectory); - } - - throw new Exception($"Could not find the solution directory going up from directory \"{assemblyDirectory}\"."); - } -} diff --git a/Cesium.TestFramework/VerifyTestBase.cs b/Cesium.TestFramework/VerifyTestBase.cs index 2b1172fc..be97c9a1 100644 --- a/Cesium.TestFramework/VerifyTestBase.cs +++ b/Cesium.TestFramework/VerifyTestBase.cs @@ -1,6 +1,5 @@ namespace Cesium.TestFramework; -[UsesVerify] public abstract class VerifyTestBase { static VerifyTestBase() diff --git a/Cesium.sln b/Cesium.sln index 47985c04..3e4e9144 100644 --- a/Cesium.sln +++ b/Cesium.sln @@ -17,6 +17,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{720F6830-E724-4B3A-BFDC-3FA1301A59D0}" + ProjectSection(SolutionItems) = preProject + .github\dependabot.yml = .github\dependabot.yml + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C08E4851-DE2E-490E-970D-4F6A0A902A98}" ProjectSection(SolutionItems) = preProject @@ -61,6 +64,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{986C6A13-2 docs\tests.md = docs\tests.md docs\type-system.md = docs\type-system.md docs\design-notes.md = docs\design-notes.md + docs\msbuild-sdk.md = docs\msbuild-sdk.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cesium.Preprocessor", "Cesium.Preprocessor\Cesium.Preprocessor.csproj", "{0CDF730D-2A2A-437F-B27F-2BB04770C709}" @@ -76,18 +80,32 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perform-common-steps", "per EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cesium.Core", "Cesium.Core\Cesium.Core.csproj", "{C83A5A9A-5667-4574-B75C-EFF38FA0FE79}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cesium.Sdk", "Cesium.Sdk\Cesium.Sdk.csproj", "{736A7F26-F507-48C5-BF48-3726EA2399CB}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cesium.Runtime.Tests", "Cesium.Runtime.Tests\Cesium.Runtime.Tests.csproj", "{526D8E61-6143-490F-BB9D-E7CD78512E55}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cesium.Compiler.Tests", "Cesium.Compiler.Tests\Cesium.Compiler.Tests.csproj", "{A5E33E79-E710-4F2F-838F-20CD0A3142F3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cesium.TestFramework.Tests", "Cesium.TestFramework.Tests\Cesium.TestFramework.Tests.csproj", "{3AF6A395-8B9C-4E97-9036-B7C03A62EC9D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cesium.Sdk.Tests", "Cesium.Sdk.Tests\Cesium.Sdk.Tests.csproj", "{02C764E4-41C6-4E73-AD93-F3DD3E1E9630}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cesium.Solution.Metadata", "Cesium.Solution.Metadata\Cesium.Solution.Metadata.csproj", "{5B59ADD9-5C9D-482C-B238-F5EFD5E1F7CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{05416E66-3615-4ABB-A130-F37EC18785A2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{F59EF607-B888-4FC2-8148-43B019F33C7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cesium.Templates.CSharp", "Cesium.Templates.CSharp\Cesium.Templates.CSharp.csproj", "{D26ED37B-1F41-4001-8755-47026763A32A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {05416E66-3615-4ABB-A130-F37EC18785A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05416E66-3615-4ABB-A130-F37EC18785A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {B85C397C-1F36-4A27-AB36-D03F55AE1A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B85C397C-1F36-4A27-AB36-D03F55AE1A89}.Debug|Any CPU.Build.0 = Debug|Any CPU {B85C397C-1F36-4A27-AB36-D03F55AE1A89}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -144,6 +162,22 @@ Global {3AF6A395-8B9C-4E97-9036-B7C03A62EC9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AF6A395-8B9C-4E97-9036-B7C03A62EC9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AF6A395-8B9C-4E97-9036-B7C03A62EC9D}.Release|Any CPU.Build.0 = Release|Any CPU + {736A7F26-F507-48C5-BF48-3726EA2399CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {736A7F26-F507-48C5-BF48-3726EA2399CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {736A7F26-F507-48C5-BF48-3726EA2399CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {736A7F26-F507-48C5-BF48-3726EA2399CB}.Release|Any CPU.Build.0 = Release|Any CPU + {02C764E4-41C6-4E73-AD93-F3DD3E1E9630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02C764E4-41C6-4E73-AD93-F3DD3E1E9630}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02C764E4-41C6-4E73-AD93-F3DD3E1E9630}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02C764E4-41C6-4E73-AD93-F3DD3E1E9630}.Release|Any CPU.Build.0 = Release|Any CPU + {5B59ADD9-5C9D-482C-B238-F5EFD5E1F7CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B59ADD9-5C9D-482C-B238-F5EFD5E1F7CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B59ADD9-5C9D-482C-B238-F5EFD5E1F7CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B59ADD9-5C9D-482C-B238-F5EFD5E1F7CF}.Release|Any CPU.Build.0 = Release|Any CPU + {D26ED37B-1F41-4001-8755-47026763A32A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D26ED37B-1F41-4001-8755-47026763A32A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D26ED37B-1F41-4001-8755-47026763A32A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D26ED37B-1F41-4001-8755-47026763A32A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -151,6 +185,7 @@ Global GlobalSection(NestedProjects) = preSolution {C08E4851-DE2E-490E-970D-4F6A0A902A98} = {720F6830-E724-4B3A-BFDC-3FA1301A59D0} {BDB20BB2-0C0E-4EEA-8D65-AF53A3E65837} = {C08E4851-DE2E-490E-970D-4F6A0A902A98} + {D26ED37B-1F41-4001-8755-47026763A32A} = {F59EF607-B888-4FC2-8148-43B019F33C7E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4EAC8E60-2623-4A2D-B08C-4326184915E7} diff --git a/Cesium.sln.DotSettings b/Cesium.sln.DotSettings index 21660cf8..5d0eed08 100644 --- a/Cesium.sln.DotSettings +++ b/Cesium.sln.DotSettings @@ -2,6 +2,7 @@ True True + True True True True diff --git a/Directory.Build.props b/Directory.Build.props index 028e277e..295e76e8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,4 +7,10 @@ nullable 12.0 + + true + + + $(MSBuildThisFileDirectory) + diff --git a/Directory.Packages.props b/Directory.Packages.props index 1bae248f..4de9e28f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,24 +3,29 @@ true - + - - + - - - + + + + - - - + + + + + + + + - + \ No newline at end of file diff --git a/README.md b/README.md index deb212e4..ebbb3369 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Documentation - [Contributor Guide][docs.contributing] - [Cesium Tests][docs.tests] - [Cesium Type System][docs.type-system] +- [Cesium SDK][docs.msbuild-sdk] - [Architecture Sets][docs.architecture-sets] - [CLI-Related Language Extensions][docs.language-extensions] - [Built-in Functions][docs.builtins] @@ -115,6 +116,7 @@ If you're interested in certain project areas, check the per-area issue labels: [docs.builtins]: docs/builtins.md [docs.contributing]: CONTRIBUTING.md [docs.design-notes]: docs/design-notes.md +[docs.msbuild-sdk]: docs/msbuild-sdk.md [docs.exceptions]: docs/exceptions.md [docs.language-extensions]: docs/language-extensions.md [docs.license]: LICENSE.md diff --git a/build.cmd b/build.cmd new file mode 100755 index 00000000..b08cc590 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..c0c0e612 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..d4a7e51e --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 00000000..31e43dcd --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.Sdk.cs b/build/Build.Sdk.cs new file mode 100644 index 00000000..dee4259a --- /dev/null +++ b/build/Build.Sdk.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.IO; +using NuGet.Packaging; +using NuGet.Versioning; +using Nuke.Common; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Serilog; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using Project = Microsoft.Build.Evaluation.Project; + +public partial class Build +{ + const string _compilerBundlePackagePrefix = "Cesium.Compiler.Bundle"; + + Target PublishAllCompilerBundles => _ => _ + .DependsOn(CompileAll) + .Executes(() => + { + var compilerProject = Solution.Cesium_Compiler.GetMSBuildProject(); + + var runtimeIds = compilerProject.GetEvaluatedProperty("RuntimeIdentifiers").Split(";"); + Log.Information( + $"Runtime identifiers defined in {Solution.Cesium_Compiler.Name}: {string.Join(", ", runtimeIds)}"); + + foreach (var runtimeId in runtimeIds) + PublishCompiler(runtimeId); + }); + + Target PublishCompilerBundle => _ => _ + .DependsOn(CompileAll) + .Executes(() => + { + PublishCompiler(EffectiveRuntimeId); + }); + + Target PackAllCompilerBundles => _ => _ + .DependsOn(PublishAllCompilerBundles) + .Executes(() => + { + var compilerProject = Solution.Cesium_Compiler.GetMSBuildProject(); + + var runtimeIds = compilerProject.GetRuntimeIds(); + Log.Information( + $"Runtime identifiers defined in {Solution.Cesium_Compiler.Name}: {string.Join(", ", runtimeIds)}"); + + foreach (var runtimeId in runtimeIds) + PackCompiler(runtimeId); + }); + + Target PackCompilerBundle => _ => _ + .DependsOn(PublishCompilerBundle) + .Executes(() => + { + PackCompiler(EffectiveRuntimeId); + }); + + Target PackSdk => _ => _ + .DependsOn(CompileAll) + .Executes(() => + { + var sdkProject = Solution.Cesium_Sdk.GetMSBuildProject(); + if (!SkipCaches && !NeedPackageSdk(sdkProject)) + { + Log.Information($"Skipping SDK packing because it was already packed. Use '--skip-caches true' to re-publish."); + return; + } + + Log.Information("Packing SDK..."); + DotNetPack(o => o + .SetConfiguration(Configuration) + .SetProject(Solution.Cesium_Sdk.Path)); + }); + + void EmitCompilerBundle(string runtimeId, Project compilerProject) + { + var version = compilerProject.GetVersion(); + var runtimePackageId = GetRuntimeBundleId(runtimeId); + var packageFile = GetRuntimeBundleFileName(version, runtimeId); + var publishDirectory = GetCompilerRuntimePublishFolder(compilerProject, runtimeId); + Directory.CreateDirectory(publishDirectory); + var publishedFiles = Directory.GetFiles(publishDirectory, "*.*", SearchOption.AllDirectories); + var packageOutputPath = compilerProject.GetPackageOutputPath(); + + Log.Debug($"Source publish directory: {publishDirectory}"); + Log.Debug($"Target package ID: {runtimePackageId}"); + Log.Debug($"Target package output directory: {packageOutputPath}"); + + var builder = new PackageBuilder + { + Id = runtimePackageId, + Version = NuGetVersion.Parse(compilerProject.GetVersion()), + Description = $"Cesium compiler native executable pack for {runtimeId} platform.", + Authors = { "Cesium Team" } + }; + builder.Files.AddRange(GetPhysicalFiles(publishDirectory, publishedFiles)); + + var packageFileName = Path.Combine(packageOutputPath, packageFile); + Log.Information($"Package is ready, saving to {packageFileName}..."); + Directory.CreateDirectory(packageOutputPath); + using var outputStream = new FileStream(packageFileName, FileMode.Create); + builder.Save(outputStream); + return; + + IEnumerable GetPhysicalFiles(string publishDirectory, IEnumerable filePaths) + { + foreach (var filePath in filePaths) + { + yield return new PhysicalPackageFile + { + SourcePath = filePath, + TargetPath = $"tools/{Path.GetRelativePath(publishDirectory, filePath)}" + }; + } + } + } + + void PublishCompiler(string runtimeId) + { + var compilerProject = Solution.Cesium_Compiler.GetMSBuildProject(); + + if (!SkipCaches && !NeedPublishCompilerBundle(compilerProject, runtimeId)) + { + Log.Information($"Skipping {runtimeId} because it was already published. Use '--skip-caches true' to re-publish."); + return; + } + + Log.Information($"Publishing for {runtimeId}, AOT {(PublishAot ? "enabled" : "disabled")}..."); + DotNetPublish(o => o + .SetConfiguration(Configuration) + .SetProject(compilerProject.ProjectFileLocation.File) + .SetRuntime(runtimeId) + .SetSelfContained(true) + .SetPublishTrimmed(PublishAot) + .SetPublishSingleFile(PublishAot) + .SetProperty("PublishAot", PublishAot) + .SetOutput(GetCompilerRuntimePublishFolder(compilerProject, runtimeId))); + } + + void PackCompiler(string runtimeId) + { + var compilerProject = Solution.Cesium_Compiler.GetMSBuildProject(); + + if (!SkipCaches && !NeedPackageCompilerBundle(compilerProject, runtimeId)) + { + Log.Information($"Skipping {runtimeId} because it was already packed. Use '--skip-caches true' to re-pack."); + return; + } + + Log.Information($"Packing compiler for {runtimeId}..."); + EmitCompilerBundle(runtimeId, compilerProject); + } + + string GetCompilerRuntimePublishFolder(Project compilerProject, string runtimeId) => + Path.Combine( + compilerProject.GetProperty("ArtifactsPath").EvaluatedValue, + compilerProject.GetProperty("ArtifactsPublishOutputName").EvaluatedValue, + Solution.Cesium_Compiler.Name, + GetRuntimeArtifactFolder(compilerProject.GetVersion(), runtimeId)); + + static string GetRuntimeArtifactFolder(string version, string runtimeId) => + $"pack_{version}_{runtimeId}"; + + static string GetRuntimeBundleId(string runtimeId) => + $"{_compilerBundlePackagePrefix}.{runtimeId}"; + + static string GetRuntimeBundleFileName(string version, string runtimeId) => + $"{_compilerBundlePackagePrefix}.{runtimeId}.{version}.nupkg"; + + bool NeedPublishCompilerBundle(Project compiler, string runtimeId) + { + var folder = GetCompilerRuntimePublishFolder(compiler, runtimeId); + + return !Directory.Exists(folder) + || Directory.GetFiles(folder, "Cesium.Compiler*").Length == 0; + } + + bool NeedPackageCompilerBundle(Project compiler, string runtimeId) + { + var version = compiler.GetVersion(); + var packageDirectory = compiler.GetPackageOutputPath(); + var packageFileName = GetRuntimeBundleFileName(version, runtimeId); + + return !File.Exists(Path.Combine(packageDirectory, packageFileName)); + } + + bool NeedPackageSdk(Project sdk) + { + var packageId = sdk.GetProperty("PackageVersion").EvaluatedValue; + var version = sdk.GetProperty("PackageId").EvaluatedValue; + var packageDirectory = sdk.GetPackageOutputPath(); + var packageFileName = $"{packageId}.{version}"; + + return !File.Exists(Path.Combine(packageDirectory, packageFileName)); + } +} diff --git a/build/Build.Templates.cs b/build/Build.Templates.cs new file mode 100644 index 00000000..0de5b747 --- /dev/null +++ b/build/Build.Templates.cs @@ -0,0 +1,18 @@ +using Nuke.Common; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +partial class Build +{ + Target PackTemplates => _ => _ + .DependsOn(CompileAll) + .Executes(() => + { + DotNetPack(_ => _ + .Apply(settings => !string.IsNullOrEmpty(RuntimeId) ? settings.SetRuntime(RuntimeId) : settings) + .SetConfiguration(Configuration) + .SetProject(Solution.Templates.Cesium_Templates_CSharp) + .EnableNoRestore()); + }); +} diff --git a/build/Build.Tests.cs b/build/Build.Tests.cs new file mode 100644 index 00000000..b31bde9a --- /dev/null +++ b/build/Build.Tests.cs @@ -0,0 +1,42 @@ +using Nuke.Common; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +partial class Build +{ + Target TestCodeGen => _ => _ + .Executes(() => ExecuteTests(Solution.Cesium_CodeGen_Tests)); + + Target TestCompiler => _ => _ + .Executes(() => ExecuteTests(Solution.Cesium_Compiler_Tests)); + + Target TestIntegration => _ => _ + .Executes(() => ExecuteTests(Solution.Cesium_IntegrationTests)); + + Target TestParser => _ => _ + .Executes(() => ExecuteTests(Solution.Cesium_Parser_Tests)); + + Target TestRuntime => _ => _ + .Executes(() => ExecuteTests(Solution.Cesium_Runtime_Tests)); + + Target TestSdk => _ => _ + .DependsOn(PackCompilerBundle) + .DependsOn(PackSdk) + .Executes(() => ExecuteTests(Solution.Cesium_Sdk_Tests)); + + Target TestAll => _ => _ + .DependsOn(TestCodeGen) + .DependsOn(TestCompiler) + .DependsOn(TestIntegration) + .DependsOn(TestParser) + .DependsOn(TestRuntime) + .DependsOn(TestSdk); + + void ExecuteTests(Project project) + { + DotNetTest(_ => _ + .SetConfiguration(Configuration) + .SetProjectFile(project.GetMSBuildProject().ProjectFileLocation.File)); + } +} diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 00000000..b6701f73 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,63 @@ +using Nuke.Common; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +partial class Build : NukeBuild +{ + public static int Main() + { + return Execute(x => x.CompileAll); + } + + [Parameter("Configuration to build - Default is 'Debug' or 'Release'")] + readonly Configuration Configuration = Configuration.Debug; + + [Parameter("If set to true, ignores all cached build results. Default: false")] + readonly bool SkipCaches = false; + + [Solution(GenerateProjects = true)] + readonly Solution Solution; + + [Parameter("If set, only executes targets for a specified runtime identifier. Provided RID must be included in property of Cesium.Compiler project.")] + readonly string RuntimeId = string.Empty; + + string EffectiveRuntimeId => !string.IsNullOrEmpty(RuntimeId) + ? RuntimeId + : Solution.Cesium_Compiler.GetProperty("DefaultAppHostRuntimeIdentifier") ?? string.Empty; + + [Parameter("If set to true, publishes compiler packs in AOT mode.")] + readonly bool PublishAot = false; + + Target Clean => _ => _ + .Before(RestoreAll) + .Executes(() => + { + DotNetClean(_ => _ + .Apply(settings => !string.IsNullOrEmpty(RuntimeId) ? settings.SetRuntime(RuntimeId) : settings)); + }); + + Target RestoreAll => _ => _ + .Executes(() => + { + DotNetRestore(_ => _ + .Apply(settings => !string.IsNullOrEmpty(RuntimeId) ? settings.SetRuntime(RuntimeId) : settings) + .SetProjectFile(Solution.FileName)); + }); + + Target CompileAll => _ => _ + .DependsOn(RestoreAll) + .Executes(() => + { + DotNetBuild(_ => _ + .Apply(settings => !string.IsNullOrEmpty(RuntimeId) ? settings.SetRuntime(RuntimeId) : settings) + .SetConfiguration(Configuration) + .SetProjectFile(Solution.FileName) + .EnableNoRestore()); + }); + + Target ForceClear => _ => _ + .OnlyWhenDynamic(() => SkipCaches) + .Before(RestoreAll); +} diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 00000000..9c08b1ae --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/Directory.Build.props b/build/Directory.Build.props new file mode 100644 index 00000000..e147d635 --- /dev/null +++ b/build/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets new file mode 100644 index 00000000..25326095 --- /dev/null +++ b/build/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/ProjectExtensions.cs b/build/ProjectExtensions.cs new file mode 100644 index 00000000..25577c9a --- /dev/null +++ b/build/ProjectExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Microsoft.Build.Evaluation; + +public static class ProjectExtensions +{ + public static IReadOnlyCollection GetRuntimeIds(this Project project) + { + return project.GetEvaluatedProperty("RuntimeIdentifiers").Split(";"); + } + + public static string GetVersion(this Project project) + { + return project.GetEvaluatedProperty("VersionPrefix"); + } + + public static string GetPackageOutputPath(this Project project) + { + return project.GetEvaluatedProperty("PackageOutputPath"); + } + + public static string GetPublishDirectory(this Project project) + { + return project.GetEvaluatedProperty("PublishDir"); + } + + public static string GetEvaluatedProperty(this Project project, string name) + { + return project.GetProperty(name).EvaluatedValue; + } +} diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 00000000..b14ba09a --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + + CS0649;CS0169 + .. + .. + 1 + true + + + + + + + + + Build.cs + + + Build.cs + + + Build.cs + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 00000000..c815d363 --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,31 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + True + True + True + True + True + True + True + True + True + True diff --git a/docs/msbuild-sdk.md b/docs/msbuild-sdk.md new file mode 100644 index 00000000..c7258b74 --- /dev/null +++ b/docs/msbuild-sdk.md @@ -0,0 +1,64 @@ +Cesium MSBuild Project SDK +-------------------------- + +Cesium provides it's own project SDK that could be used to simplify building of Cesium programs and libraries. + +Cesium MSBuild SDK inherits default behavior from a `Microsoft.NET.Sdk` SDK and tries to integrate with it the same way as C# does. + +Cesium MSBuild SDK only supports SDK-style projects. + +> Note: Some of the common MSBuild properties and items those are not stated in this document could be also used in Cesium project files. Not all of them are tested so something may not work as expected. + +### Source files +Source files are defined with `` items, very similar to other .NET languages: +```xml + + + + +``` +> Note: In the current SDK implementation, source files will not be included into compilation implicitly. They should be defined in `` items, in opposite to SDK-style C# projects, where all C# source files are implicitly added to the compilation. + +### References + +#### Packages +Not supported yet. + +#### Projects +Not supported yet. + +#### Assemblies +Not supported yet. + +### Preprocessor directives +`` property is directly mapped to a list of preprocessor items. So, you could define such constants in `.ceproj`: +```xml + + $(DefineConstants);FOO;BAR + +``` + +And then use it in your .c code: +```c +#ifdef FOO +int foo() { return 0; } +#endif + +#ifdef BAR +int bar() { return 1; } +#endif +``` + +### Output files +Output assembly and additional artifacts will be placed in `bin` folder. Depending on the target framework, output type and a platform you're compiling on, `.runtimeconfig.json` and `.deps.json` files will also be generated. + +### Properties +- `SkipCesiumCompilerInstallation`: if set to `true`, doesn't automatically install a compiler bundle package. In that case it should be explicitly provided by `CesiumCompilerPackageName` and `CesiumCompilerPackageVersion` properties. Default: `false` +- `SkipCesiumRuntimeInstallation`: if set to `true`, doesn't automatically install a `Cesium.Runtime` package. In that case it should be explicitly installed. Default: `false` +- `CesiumCompilerPackageName`: an optional platform-specific compiler bundle package name. Should be specified if `SkipCesiumCompilerInstallation` set to `true`. Default: `Cesium.Compiler.Pack.{RID}` +- `CesiumCompilerPackageName`: an optional platform-specific compiler bundle package version. Should be specified if `SkipCesiumCompilerInstallation` set to `true`. Default: Cesium SDK version +- `CesiumCompilerPath`: an optional path to compiler executable. Use this property to specify a path to the compiler not coming from a compiler package. +- `CesiumCoreLibAssemblyPath`: an optional path to .NET runtime assembly: `System.Runtime` or `mscorlib`, depending on the target framework. + +### Items +- `Compile`: a C source file to be included into compiler execution command diff --git a/docs/tests.md b/docs/tests.md index f27e486d..be231734 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -3,16 +3,32 @@ Cesium Tests Being a compiler, Cesium requires a complicated test suite checking every feature. -There are two kinds of tests in Cesium: unit tests (directly calling various internal APIs in the compiler) and integration tests (interacting with the compiler executable and comparing the resulting programs' behavior with programs compiled by other compilers). +There are three kinds of tests in Cesium: unit tests (directly calling various internal APIs in the compiler), integration tests (interacting with the compiler executable and comparing the resulting programs' behavior with programs compiled by other compilers) and SDK tests (testing integration with MSBuild via MSBuild project SDK). -Unit Tests ----------- -Unit tests in Cesium are normal .NET tests, so they are runnable by the following shell command: -s +Running Tests +------------- +To run all tests from solution, make sure to restore locally installed tools: +```console +dotnet tool restore +``` + +Then, run `TestAll` target using NUKE: ```console -$ dotnet test +dotnet nuke TestAll ``` +You could also execute test from specific corresponding test projects: +- `dotnet nuke TestParser` +- `dotnet nuke TestCompiler` +- `dotnet nuke TestCodeGen` +- `dotnet nuke TestRuntime` +- `dotnet nuke TestIntegration` +- `dotnet nuke TestSdk` + +Unit Tests +---------- +Unit tests in Cesium are normal .NET tests. + There are two kinds of unit tests: a few of "the real" unit tests (e.g. `Cesium.Parser.Tests.LexerTests.IdentifierTests`) and a set of [characterization tests][wiki.characterization-tests]. The real unit tests verify certain facts using assertions of the Xunit testing framework, but their usage in the compiler is small. The characterization tests, on the other hand, invoke parts of the compiler on various sources, and then dump the results (e.g. a full parse tree of a code fragment, or a whole compiled assembly). The characterization tests comprise the biggest part of the compiler test suite, and it helps us to thoroughly verify the most aspects of the compiler behavior. @@ -53,3 +69,30 @@ There are two categories of integration tests: .NET interop tests and compiler v [wiki.characterization-tests]: https://en.wikipedia.org/wiki/Characterization_test +SDK Tests +--------- +#### How SDK tests work +SDK tests check correctness of integration with MSBuild. They are focused on build output, including artifacts existence asserting. + +SDK tests (and Cesium Project SDK itself) require compiler bundle to be built and packed. A compiler bundle is a special platform-specific NuGet package containing a published compiler executable with dependencies. It is not intended to be used as a runtime dependency and only used while building project. + +Compiler packing is done by 2 NUKE targets: +- `PublishCompilerBundle`: a target that make platform-specific `dotnet publish` of compiler bundle to corresponding artifacts' folder. +- `PackCompilerBundle`: a target that wraps a published compiler bundle into a NuGet package which is then used by SDK to deliver compiler to user's project + +If you want to run these tests without Nuke (e.g. from the IDE), run the targets using a shell command: +```console +$ dotnet nuke PackCompilerBundle +``` +After that, run the tests in your preferred way. + +Both targets are called automatically when `TestSdk` target is invoked. + +SDK itself should also be built to be used in test projects. This is done by dependent target `PackSdk` which produces `Cesium.Sdk` NuGet package, suitable as Project SDK. + +Having all necessary dependencies, SDK tests are invoking `dotnet build` CLI with test projects, representing different layouts and configurations. Test result is determined by MSBuild output and artifacts presence. + +#### Adding new tests +Adding new tests is quite straightforward. +1. Add a test project if needed to the `TestProjects` directory. All items from that folder will be automatically included into temporary test execution directory. +2. Write a test with the new test project in use. Look for the examples at `CesiumCompileTests.cs`. diff --git a/global.json b/global.json new file mode 100644 index 00000000..36394634 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..32d08817 --- /dev/null +++ b/nuget.config @@ -0,0 +1,9 @@ + + + + + + + + +