diff --git a/.github/workflows/acceptance-test.yaml b/.github/workflows/acceptance-test.yaml new file mode 100644 index 00000000..e17e3958 --- /dev/null +++ b/.github/workflows/acceptance-test.yaml @@ -0,0 +1,38 @@ +name: Acceptance Test + +on: + workflow_dispatch: + pull_request: + + +jobs: + acceptance-test-msbuild: + runs-on: windows-2022 + strategy: + matrix: + test_suite: + - name: OutProc + path: outproc + - name: 32 Bit + path: 32bit + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.300 + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release + + - name: Run ${{ matrix.test_suite.Name }} Acceptance test + run: .\msbuild-acceptance-test.cmd + working-directory: .\examples\${{ matrix.test_suite.path }}\scripts + continue-on-error: false diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4f8ef17c..f87ca9cf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,14 +24,11 @@ jobs: - name: Build run: dotnet build -c Release - - name: Check code format (editorconfig) - run: dotnet format --verify-no-changes - - - name: Test (Release) - run: dotnet test -c Release --no-build --verbosity normal - - name: Nuget pack library run: dotnet pack -c Release src/dscom/dscom.csproj - name: Nuget pack tool run: dotnet pack -c Release src/dscom.client/dscom.client.csproj + + - name: Nuget pack build tools + run: dotnet pack -c Release src/dscom.build/dscom.build.csproj diff --git a/.github/workflows/code-style.yaml b/.github/workflows/code-style.yaml new file mode 100644 index 00000000..d0bec3aa --- /dev/null +++ b/.github/workflows/code-style.yaml @@ -0,0 +1,26 @@ +name: Check code style + +on: + workflow_dispatch: + pull_request: + +jobs: + check-code-style: + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.300 + + - name: Restore dependencies + run: dotnet restore + + - name: Check code format (editorconfig) + run: dotnet format --verify-no-changes + continue-on-error: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ff3d932c..8c1ba310 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -48,6 +48,13 @@ jobs: run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET }} --source https://api.nuget.org/v3/index.json working-directory: src/dscom.client/bin/Release + - name: Nuget pack build tools + run: dotnet pack -c Release src/dscom.build/dscom.build.csproj + + - name: Nuget push build tools + run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET }} --source https://api.nuget.org/v3/index.json + working-directory: src/dscom.build/bin/Release + - name: Publish 32Bit binary run: dotnet publish .\src\dscom.client\dscom.client.csproj --no-self-contained -c Release -r win-x86 -f net6.0 /p:PublishSingleFile=true; copy src\dscom.client\bin\Release\net6.0\win-x86\publish\dscom.exe src\dscom.client\bin\Release\net6.0\win-x86\publish\dscom32.exe diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml new file mode 100644 index 00000000..7af0419e --- /dev/null +++ b/.github/workflows/unit-test.yaml @@ -0,0 +1,29 @@ +name: Run Unit Tests + +on: + workflow_dispatch: + pull_request: + +jobs: + unit-test: + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.300 + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release + + - name: Unit test + run: dotnet test --no-build -c Release + continue-on-error: false diff --git a/.gitignore b/.gitignore index 44dfe9c5..27619f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ Release/ test.results/ .tmp/ _temporary.cs -.publish/ \ No newline at end of file +.publish/ +_packages +*.binlog diff --git a/README.md b/README.md index 9bd17a6d..8dd58d25 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ From the Microsoft documentation: One main goal is to make `dscom` behave like `tlbexp.exe`. _Happy IUnknowing and IDispatching ;-)_ + - [dSPACE COM tools](#dspace-com-tools) - [Command Line Client](#command-line-client) - [Installation](#installation) @@ -33,6 +34,15 @@ _Happy IUnknowing and IDispatching ;-)_ - [Migration notes (mscorelib vs System.Private.CoreLib)](#migration-notes-mscorelib-vs-systemprivatecorelib) - [Why can I load a .NET Framework library into a .NET application?](#why-can-i-load-a-net-framework-library-into-a-net-application) - [Limitations](#limitations) + - [Build Tasks](#build-tasks) + - [Preface](#preface) + - [Build task usage](#build-task-usage) + - [Using the native build task](#using-the-native-build-task) + - [Using the CLI based task](#using-the-cli-based-task) + - [Enforcing the usage of the CLI](#enforcing-the-usage-of-the-cli) + - [Enforcing to stop the build, if an error occurs](#enforcing-to-stop-the-build-if-an-error-occurs) + - [Parameters](#parameters) + ## Command Line Client The command-line interface (CLI) tool `dscom` is a replacement for `tlbexp.exe` and `OleView` (View TypeLib). @@ -255,3 +265,87 @@ classextern forwarder System.Exception - Color is converted to `OLE_COLOR`(stdole) - No support for `UnmanagedType.CustomMarshaler` - No support for .NET Framework assemblies with `AssemblyMetadataAttribute` value ".NETFrameworkAssembly" + +## Build Tasks + +### Preface + +The `dSPACE.Runtime.InteropServices.BuildTasks` assembly and NuGet package provide the ability to create type libraries for a certain assembly at runtime. + +For details on the implementation refer to the [documentation](/src/dscom.build/docs/ReadMe.md) section of the repository. + +### Build task usage + +To create a type library at compile time, simply add a reference to the nuget package, e.g. by using the command line. + +```shell +$ dotnet add package dSPACE.Runtime.InteropServices.Build +... +$ +``` + +The result should be a line as follows in your `.csproj` file: + +```xml + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + +``` + +**Note**: The extra attribute `NoWarn="NU1701"` is only required, if neither `.NET 4.8` nor `.NET 6.0` are targeted, since dotnet pack will currently not create a .NETStandard 2.0 compliant NuGet Package. + +#### Using the native build task + +The native build task is automatically selected, if a .NET 4.8 or .NET 6.0 assembly for Windows is being build using an x64 platform. + +#### Using the CLI based task + +The CLI task is automatically selected, if a .NET Standard 2.0 assembly is build. It is also chosen, if the target platform is set to x86. + +#### Enforcing the usage of the CLI + +It might be necessary to select the CLI based task. To do so, add the following property to your `.csproj` file: + +```XML +<_DsComForceToolUsage>true +``` + +This will enforce the usage of the DsCom as a command-line tool. Please note, that verbose logging will no longer be working. + +#### Enforcing to stop the build, if an error occurs + +The build tasks puts a warning to the build log, if the desired type library has not been created, even if the backend has reported a success. + +This warning is issued with the warning code `DSCOM001`, which can be collected in the `WarningsAsErrors` array: + +```XML +$(WarningsAsErrors);DSCOM001 +``` + +This way the build stops, if the type library is not exported. + +### Parameters + +The build task can be parameterized with the following properties: + +| **Name** | **Description** | **Default** | +| ------------- | -------------------- | ----------- | +| _DsComTlbExt | Extension of the resulting type library. | .tlb | +| _DsComForceToolUsage | Use DsCom Exe files to create the TLB | false | +| DsComTypeLibraryUniqueId | Overwrite the library UUID | Empty Guid | +| DsComRegisterTypeLibrariesAfterBuild | Use regasm call after the build to register type library after the build | false | +| DsComTlbExportAutoAddReferences | Add referenced assemblies automatically to type libraries | true | +| DsComTlbExportIncludeReferencesWithoutHintPath | If a `Reference` assembly does not provide a `HintPath` Metadata, the item spec shall be task. | false | +| _DsComExportTypeLibraryTargetFile | Path to the resulting file. | `$(TargetDir)\$(TargetName)$(_DsComTlbExt)` * | +| _DsComExportTypeLibraryAssemblyFile | Path to the source assembly file. | `$(TargetPath)` * | + +*) This value cannot be overridden. + +The build task consumes the following items: + +| **Name** | **Description** | +| ---------------------------- | -------------------------------- | +| DsComTlbExportTlbReferences | Referenced type library files. | +| DsComTlbExportReferencePaths | Directories containing type libraries to use for export. | +| DsComTlbExportAssemblyPaths | Assemblies to add for the export. | diff --git a/dscom.sln b/dscom.sln index a7a24b89..c343db29 100644 --- a/dscom.sln +++ b/dscom.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "assembly3", "src\dscom.demo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dscom.test", "src\dscom.test\dscom.test.csproj", "{5B402A1B-18B1-4D88-804A-BC0E58EF3730}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dscom.build", "src\dscom.build\dscom.build.csproj", "{F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,10 @@ Global {1BAB2994-1423-433B-BFF0-F4698B6D4A04}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BAB2994-1423-433B-BFF0-F4698B6D4A04}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BAB2994-1423-433B-BFF0-F4698B6D4A04}.Release|Any CPU.Build.0 = Release|Any CPU + {F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {54297095-6CB9-4A75-B9C0-41ADA916C27C} = {0A2E33B4-9DF7-4199-BC39-B0FC9C99FA97} @@ -61,5 +67,6 @@ Global {B55E3947-1338-4EDF-A802-B24D91636B31} = {CAAB6257-7EC0-484E-9593-B60CEE8F47D1} {5B402A1B-18B1-4D88-804A-BC0E58EF3730} = {0A2E33B4-9DF7-4199-BC39-B0FC9C99FA97} {1BAB2994-1423-433B-BFF0-F4698B6D4A04} = {CAAB6257-7EC0-484E-9593-B60CEE8F47D1} + {F8F68E57-CFFE-4EA5-9C1A-2CD9223B5D85} = {0A2E33B4-9DF7-4199-BC39-B0FC9C99FA97} EndGlobalSection EndGlobal diff --git a/examples/32bit/comtestdotnet/comtestdotnet.csproj b/examples/32bit/comtestdotnet/comtestdotnet.csproj index 273af8c9..4cb3f2bf 100644 --- a/examples/32bit/comtestdotnet/comtestdotnet.csproj +++ b/examples/32bit/comtestdotnet/comtestdotnet.csproj @@ -1,5 +1,10 @@  + + false + true + + net6.0;net48 10.0 @@ -8,15 +13,16 @@ disabled enable true + $(WarningsAsErrors);DSCOM001 + - + - \ No newline at end of file diff --git a/examples/32bit/scripts/msbuild-acceptance-test.cmd b/examples/32bit/scripts/msbuild-acceptance-test.cmd new file mode 100644 index 00000000..b040ed86 --- /dev/null +++ b/examples/32bit/scripts/msbuild-acceptance-test.cmd @@ -0,0 +1,72 @@ +@ECHO off + +SET root=%~dp0\..\..\..\ + +PUSHD %root% + +dotnet pack -p:Configuration=Release + +IF NOT EXIST %root%\_packages MKDIR _packages + +XCOPY /Y /I /C /F .\src\dscom\bin\Release\*.nupkg _packages\ +XCOPY /Y /I /C /F .\src\dscom.build\bin\Release\*.nupkg _packages\ + +POPD + +PUSHD %~dp0\..\comtestdotnet + +dotnet msbuild -nodeReuse:False -t:Clean -p:Configuration=Release -p:PerformAcceptanceTest=Runtime + +dotnet msbuild -nodeReuse:False -t:Clean -p:Configuration=Release -p:PerformAcceptanceTest=Runtime + +dotnet add comtestdotnet.csproj package --prerelease -s %root%\_packages dSPACE.Runtime.InteropServices.BuildTasks + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:Platform=x64 -p:TargetPlatform=net48 -p:PerformAcceptanceTest=Runtime +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:Platform=x64 -p:TargetPlatform=net48 -p:PerformAcceptanceTest=Runtime -bl +SET ERRUNTIMEX64_NET48=%ERRORLEVEL% + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:Platform=x64 -p:TargetPlatform=net6.0-windows -p:PerformAcceptanceTest=Runtime +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:Platform=x64 -p:TargetPlatform=net6.0-windows -p:PerformAcceptanceTest=Runtime -bl +SET ERRUNTIMEX64_NET60=%ERRORLEVEL% + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:Platform=x86 -p:TargetPlatform=net48 -p:PerformAcceptanceTest=Runtime +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:Platform=x86 -p:TargetPlatform=net48 -p:PerformAcceptanceTest=Runtime -bl +SET ERRUNTIMEX86_NET48=%ERRORLEVEL% + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:Platform=x86 -p:TargetPlatform=net6.0-windows -p:PerformAcceptanceTest=Runtime +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:Platform=x86 -p:TargetPlatform=net6.0-windows -p:PerformAcceptanceTest=Runtime -bl +SET ERRUNTIMEX86_NET60=%ERRORLEVEL% + +dotnet remove comtestdotnet.csproj package dSPACE.Runtime.InteropServices.BuildTasks + +POPD + +SetLocal EnableDelayedExpansion + +SET EXITCODE=0 + +IF NOT "%ERRUNTIMEX64_NET48%" == "0" ( + SET EXITCODE=1 + ECHO "Runtime specific acceptance test for platform x64 using .NET FullFramework 4.8 failed." +) + +IF NOT "%ERRUNTIMEX64_NET60%" == "0" ( + SET EXITCODE=1 + ECHO "Runtime specific acceptance test for platform x64 using .NET 6.0 failed." +) + +IF NOT "%ERRUNTIMEX86_NET48%" == "0" ( + SET EXITCODE=1 + ECHO "Runtime specific acceptance test for platform x86 using .NET FullFramework 4.8 failed." +) + +IF NOT "%ERRUNTIMEX86_NET60%" == "0" ( + SET EXITCODE=1 + ECHO "Runtime specific acceptance test for platform x64 using .NET 6.0 failed." +) + +IF "%EXITCODE%" == "0" ( + ECHO "Acceptance test completed successfully." +) + +EXIT /B %EXITCODE% diff --git a/examples/outproc/scripts/msbuild-acceptance-test.cmd b/examples/outproc/scripts/msbuild-acceptance-test.cmd new file mode 100644 index 00000000..c97fb3be --- /dev/null +++ b/examples/outproc/scripts/msbuild-acceptance-test.cmd @@ -0,0 +1,56 @@ +@ECHO off + +SET root=%~dp0\..\..\..\ + +PUSHD %root% + +dotnet pack -p:Configuration=Release + +IF NOT EXIST %root%\_packages MKDIR _packages + +XCOPY /Y /I /C /F .\src\dscom\bin\Release\*.nupkg _packages\ +XCOPY /Y /I /C /F .\src\dscom.build\bin\Release\*.nupkg _packages\ + +POPD + +PUSHD %~dp0\.. + +dotnet msbuild -nodeReuse:False -t:Clean -p:Configuration=Release -p:PerformAcceptanceTest=Runtime + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:PerformAcceptanceTest=Runtime + +dotnet add server\common\contract.csproj package --prerelease -s %root%\_packages dSPACE.Runtime.InteropServices.BuildTasks + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:PerformAcceptanceTest=Runtime +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:PerformAcceptanceTest=Runtime -bl +SET ERRUNTIME=%ERRORLEVEL% + +dotnet msbuild -nodeReuse:False -t:Clean -p:Configuration=Release -p:PerformAcceptanceTest=Runtime + +dotnet msbuild -nodeReuse:False -t:Restore -p:Configuration=Release -p:PerformAcceptanceTest=NetStandard +dotnet msbuild -nodeReuse:False -t:Build -p:Configuration=Release -p:PerformAcceptanceTest=NetStandard -bl +SET ERSTANDARD=%ERRORLEVEL% + +dotnet remove server\common\contract.csproj package dSPACE.Runtime.InteropServices.BuildTasks + +POPD + +SetLocal EnableDelayedExpansion + +SET EXITCODE=0 + +IF NOT "%ERRUNTIME%" == "0" ( + SET EXITCODE=1 + ECHO "Runtime specific acceptance test failed." +) + +IF NOT "%ERSTANDARD%" == "0" ( + SET EXITCODE=1 + ECHO ".NET Standard 2.0 specific acceptance test failed." +) + +IF "%EXITCODE%" == "0" ( + ECHO "Acceptance test completed successfully." +) + +EXIT /B %EXITCODE% diff --git a/examples/outproc/server/common/RegistryHelper.cs b/examples/outproc/server/common/RegistryHelper.cs index aaecb201..13683fb9 100644 --- a/examples/outproc/server/common/RegistryHelper.cs +++ b/examples/outproc/server/common/RegistryHelper.cs @@ -25,7 +25,7 @@ public static void RegisterOutProcServer(string versionIndependentProgId, str exePath +=".exe"; } string progId = $"{versionIndependentProgId}.{version}"; - GuidAttribute guid = (GuidAttribute)typeof(T).GetCustomAttributes().FirstOrDefault(); + GuidAttribute? guid = (GuidAttribute?)typeof(T).GetCustomAttributes().FirstOrDefault(); if (guid == null) { throw new ArgumentException("Coclass guid not set!"); @@ -93,7 +93,7 @@ public static void RegisterOutProcServer(string versionIndependentProgId, str public static void UnregisterOutProcServer(string versionIndependentProgId, string version) { - GuidAttribute guid = (GuidAttribute)typeof(T).GetCustomAttributes().FirstOrDefault(); + GuidAttribute? guid = (GuidAttribute?)typeof(T).GetCustomAttributes().FirstOrDefault(); if (guid == null) { throw new ArgumentException("Coclass guid not set!"); diff --git a/examples/outproc/server/common/contract.csproj b/examples/outproc/server/common/contract.csproj index 329741c9..a5939d8d 100644 --- a/examples/outproc/server/common/contract.csproj +++ b/examples/outproc/server/common/contract.csproj @@ -1,18 +1,30 @@ - netstandard2.0 + false + true + + + + netstandard2.0 + net6.0-windows;net48 enable enable 10.0 + $(WarningsAsErrors);DSCOM001 - + + + + + $(NoWarn);NU1701 + diff --git a/src/dscom.build/DefaultBuildContext.cs b/src/dscom.build/DefaultBuildContext.cs new file mode 100644 index 00000000..0b3d492d --- /dev/null +++ b/src/dscom.build/DefaultBuildContext.cs @@ -0,0 +1,359 @@ +// Copyright 2022 dSPACE GmbH, Carsten Igel and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; +#if NET5_0_OR_GREATER +using System.Runtime.Loader; +#endif +using System.Security; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using COMException = System.Runtime.InteropServices.COMException; +using SystemInteropServices = System.Runtime.InteropServices; + +namespace dSPACE.Runtime.InteropServices.BuildTasks; + +/// +/// Default implementation of the interface +/// using as implementation for conversion +/// and as implementation for +/// event handling. +/// +internal sealed class DefaultBuildContext : IBuildContext +{ + /// + public bool IsRunningOnWindows => SystemInteropServices.RuntimeInformation.IsOSPlatform(SystemInteropServices.OSPlatform.Windows); + +#if NET5_0_OR_GREATER + /// + public string RuntimeDescription => $"{SystemInteropServices.RuntimeInformation.OSDescription} {SystemInteropServices.RuntimeInformation.OSArchitecture} ({SystemInteropServices.RuntimeInformation.ProcessArchitecture} [{SystemInteropServices.RuntimeInformation.RuntimeIdentifier} - {SystemInteropServices.RuntimeInformation.FrameworkDescription}])"; +#else + /// + public string RuntimeDescription => $"{SystemInteropServices.RuntimeInformation.OSDescription} {SystemInteropServices.RuntimeInformation.OSArchitecture} ({SystemInteropServices.RuntimeInformation.ProcessArchitecture} [{SystemInteropServices.RuntimeInformation.FrameworkDescription}])"; +#endif + + /// + public bool EnsureFileExists(string? fileNameAndPath) + { + return File.Exists(fileNameAndPath); + } + + /// + public bool EnsureDirectoryExists(string? directoryPath) + { + return Directory.Exists(directoryPath); + } + + /// + public bool ConvertAssemblyToTypeLib(TypeLibConverterSettings settings, TaskLoggingHelper log) + { + // Load assembly from file. +#if NET5_0_OR_GREATER + var loadContext = CreateLoadContext(settings, log); + var assembly = LoadAssembly(settings, loadContext, log); +#else + log.LogWarning("Unloading assemblies is only supported with .NET 5 or later. Please remove all remaining MsBuild processes before rebuild."); + var assembly = LoadAssembly(settings, log); +#endif + + if (assembly is null) + { + log.LogWarning("Failed to load assembly {0}. Task failed.", settings.Assembly); + return false; + } + + try + { + // Create type library converter. + var converter = new TypeLibConverter(); + + // Create event handler. + var sink = new LoggingTypeLibExporterSink(log); + // create conversion. + var tlb = converter.ConvertAssemblyToTypeLib(assembly, settings, sink); + if (tlb == null) + { + log.LogError("The following type library could not be created successfully: {0}. Reason: Operation was not successful.", settings.Out); + } + else + { + log.LogMessage(MessageImportance.High, "Finished generation of the following type library: {0}", settings.Out); + } + + if (!File.Exists(settings.Out)) + { + log.LogWarning( + null, + "DSCOM001", + null, + null, + 0, + 0, + 0, + 0, + "Could not find the type library at the following location: {0}", settings.Out); + } + + return tlb != null; + } + catch (COMException e) + { + log.LogErrorFromException(e, false, true, settings.Assembly); + return false; + } +#if NET5_0_OR_GREATER + finally + { + try + { + loadContext.Unload(); + } + catch (InvalidOperationException) + { + log.LogWarning("Failed to unload the assembly load context."); + } + } +#endif + } + +#if NET5_0_OR_GREATER + /// + /// Creates an instance of that will + /// take care of the loading and unloading the target assemblies and + /// can be unloaded afterwards. + /// + /// The type library settings. + /// An that can be unloaded. + private static AssemblyLoadContext CreateLoadContext(TypeLibConverterSettings settings, TaskLoggingHelper log) + { + var loadContext = new AssemblyLoadContext($"msbuild-load-ctx-{Guid.NewGuid()}", true); + loadContext.Resolving += (ctx, name) => + { + if (TryResolveAssemblyFromSettings(name.Name ?? string.Empty, settings, log, out var assemblyPath)) + { + return ctx.LoadFromAssemblyPath(assemblyPath); + } + + log.LogWarning("Failed to resolve {0} from the following files: {1}", name.Name, string.Join(", ", settings.ASMPath)); + + return default; + }; + + return loadContext; + } + + /// + /// Tries to load the assembly specified in the using the specified + /// . If the assembly cannot be loaded, the result will be null. + /// + /// The settings. + /// The assembly load context. + /// The log to write messages to. + /// The assembly loaded. + private static Assembly? LoadAssembly(TypeLibConverterSettings settings, AssemblyLoadContext loadContext, TaskLoggingHelper log) + { + Assembly assembly; + try + { + assembly = loadContext.LoadFromAssemblyPath(settings.Assembly); + } + catch (Exception e) when + (e is ArgumentNullException + or FileNotFoundException + or FileLoadException + or BadImageFormatException + or SecurityException + or ArgumentException + or PathTooLongException) + { + log.LogErrorFromException(e, true, true, settings.Assembly); + try + { + loadContext.Unload(); + } + catch (InvalidOperationException) + { + log.LogWarning("Failed to unload the following assembly: {0}.", settings.Assembly); + } + + return default; + } + + return assembly; + } +#else + /// + /// Tries to load the assembly specified in the using the current + /// . If the assembly cannot be loaded, the result will be null. + /// + /// The settings. + /// The log to write messages to. + /// The assembly loaded. + private static Assembly? LoadAssembly(TypeLibConverterSettings settings, TaskLoggingHelper log) + { + try + { + var content = File.ReadAllBytes(settings.Assembly); + var appDomain = AppDomain.CurrentDomain; + var resolveHandler = CreateResolveHandler(settings, log); + try + { + appDomain.AssemblyResolve += resolveHandler; + return appDomain.Load(content); + } + finally + { + appDomain.AssemblyResolve -= resolveHandler; + } + } + catch (Exception e) when + (e is ArgumentNullException + or PathTooLongException + or DirectoryNotFoundException + or IOException + or UnauthorizedAccessException + or FileNotFoundException + or NotSupportedException + or SecurityException) + { + log.LogErrorFromException(e, true, true, settings.Assembly); + return default; + } + } + + /// + /// Creates an that tries to look up a dependent assembly. + /// + /// The conversion settings. + /// The task logging helper. + /// A new resolve event handler instance. + private static ResolveEventHandler CreateResolveHandler(TypeLibConverterSettings settings, TaskLoggingHelper log) + { + Assembly? AssemblyResolveClosure(object? sender, ResolveEventArgs args) + { + if (TryResolveAssemblyFromSettings(args.Name ?? string.Empty, settings, log, out var assemblyPath)) + { + return AppDomain.CurrentDomain.Load(assemblyPath); + } + + log.LogWarning("Failed to resolve assembly: {0}", args.Name); + return default; + } + + return AssemblyResolveClosure; + } +#endif + + /// + /// Tries to resolve the managed assembly with the specified from the . + /// If this method returns true, the resolved file path will be written to . + /// + /// The name of the assembly file to load (without extension). + /// The settings for type conversions. + /// The logging helper. + /// Path to resolved file. + /// true, if the assembly could be resolved; false otherwise. + private static bool TryResolveAssemblyFromSettings(string assemblyFileName, TypeLibConverterSettings settings, TaskLoggingHelper log, out string assemblyPath) + { + var validAssemblyExtensions = new string[] { ".dll", ".exe" }; + if (TryResolveAssemblyFromReferencedFiles(assemblyFileName, settings, log, validAssemblyExtensions, out assemblyPath) + || TryResolveAssemblyFromAdjacentFiles(assemblyFileName, settings, log, validAssemblyExtensions, out assemblyPath)) + { + return true; + } + + log.LogWarning("Failed to resolve {0} from the following files: {1}", assemblyFileName, string.Join(", ", settings.ASMPath)); + assemblyPath = string.Empty; + return false; + } + + /// + /// Tries to resolve the managed assembly with the specified from the + /// using the property to identify the assembly. + /// If this method returns true, the resolved file path will be written to . + /// + /// The name of the assembly file to load (without extension). + /// The settings for type conversions. + /// The logging helper. + /// Any extension that might be considered as valid assembly extension. + /// Path to resolved file. + /// true, if the assembly could be resolved; false otherwise. + private static bool TryResolveAssemblyFromReferencedFiles(string assemblyFileName, TypeLibConverterSettings settings, TaskLoggingHelper log, string[] validAssemblyExtensions, out string assemblyPath) + { + log.LogMessage(MessageImportance.Low, "Trying to resolve assembly {0} from referenced files.", assemblyFileName); + foreach (var path in settings.ASMPath) + { + var currentAssemblyFileName = Path.GetFileName(path) ?? string.Empty; + log.LogMessage(MessageImportance.Low, "Current file is {0}. Maybe it matches.", path); + foreach (var extension in validAssemblyExtensions) + { + var possibleFileName = assemblyFileName + extension; + log.LogMessage(MessageImportance.Low, "Trying to resolve assembly {0} as {1}.", assemblyFileName, possibleFileName); + if (StringComparer.InvariantCultureIgnoreCase.Equals(possibleFileName, currentAssemblyFileName) + && File.Exists(path)) + { + log.LogMessage(MessageImportance.Low, "Assembly resolved as {0}.", path); + assemblyPath = path; + return true; + } + } + } + + assemblyPath = string.Empty; + return false; + } + + /// + /// Tries to resolve the managed assembly with the specified from the + /// using the property to look up directories that might contain the file. + /// If this method returns true, the resolved file path will be written to . + /// + /// The name of the assembly file to load (without extension). + /// The settings for type conversions. + /// The logging helper. + /// Any extension that might be considered as valid assembly extension. + /// Path to resolved file. + /// true, if the assembly could be resolved; false otherwise. + private static bool TryResolveAssemblyFromAdjacentFiles(string assemblyFileName, TypeLibConverterSettings settings, TaskLoggingHelper log, string[] validAssemblyExtensions, out string assemblyPath) + { + log.LogMessage(MessageImportance.Low, "Trying to resolve assembly {0} from adjacent files.", assemblyFileName); + foreach (var path in settings.ASMPath) + { + var currentAssemblyFileName = Path.GetFileName(path) ?? string.Empty; + var assemblyDirectoryName = Path.GetDirectoryName(path) ?? string.Empty; + if (string.IsNullOrWhiteSpace(assemblyDirectoryName)) + { + continue; + } + + log.LogMessage(MessageImportance.Low, "Current directory to look at is {0}. Maybe it matches.", assemblyDirectoryName); + foreach (var extension in validAssemblyExtensions) + { + var possibleFileName = assemblyFileName + extension; + var possibleAssemblyFilePath = Path.Combine(assemblyDirectoryName, possibleFileName); + log.LogMessage(MessageImportance.Low, "Trying to resolve assembly {0} as {1}.", assemblyFileName, possibleAssemblyFilePath); + if (File.Exists(possibleAssemblyFilePath)) + { + log.LogMessage(MessageImportance.Low, "Assembly resolved as {0}.", possibleAssemblyFilePath); + assemblyPath = possibleAssemblyFilePath; + return true; + } + } + } + + assemblyPath = string.Empty; + return false; + } +} diff --git a/src/dscom.build/DsComPackaging.targets b/src/dscom.build/DsComPackaging.targets new file mode 100644 index 00000000..24e4047b --- /dev/null +++ b/src/dscom.build/DsComPackaging.targets @@ -0,0 +1,148 @@ + + + + + $(MsBuildThisFileDirectory)..\dscom.client\ + $(MsBuildThisFileDirectory)obj\tools\ + PackForBuildTaskTools=true;PublishSingleFile=true;SelfContained=false;Configuration=Release;TargetFramework=$(DsComBuildTargetFrameWork) + + + + + $(GenerateNuspecDependsOn);DsComBuildPackTaskDependencies;DsComBuildPackTool + + + + + + + <_PackageFiles Include="$(MsBuildThisFileDirectory)\bin\$(Configuration)\*\dSPACE.Runtime.InteropServices.dll"> + build\_dscom%(RecursiveDir) + false + Content + + + + + + + + <_PackageFiles Include="$(DsComClientToolsTargetDir)\x64\dscom.exe"> + tools\x64\ + false + Content + + <_PackageFiles Include="$(DsComClientToolsTargetDir)\x86\dscom.exe"> + tools\x86\ + false + Content + + + + + + + + <_TargetFrameworks Include="netstandard2.0" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/FileSystemChecks.cs b/src/dscom.build/FileSystemChecks.cs new file mode 100644 index 00000000..4fb16e57 --- /dev/null +++ b/src/dscom.build/FileSystemChecks.cs @@ -0,0 +1,161 @@ +// Copyright 2022 dSPACE GmbH, Carsten Igel and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Build.Utilities; + +namespace dSPACE.Runtime.InteropServices.BuildTasks; + +/// +/// Implementation of file system contracts and the corresponding checks using the logging mechanisms of MsBuild. +/// +internal sealed class FileSystemChecks +{ + /// + /// The logging mechanism from the task. + /// + private readonly TaskLoggingHelper _log; + + /// + /// The build context. + /// + private readonly IBuildContext _context; + + /// + /// Creates a new instance of the class + /// using the supplied to write messages to. + /// + /// The log to apply. + /// The context to apply. + internal FileSystemChecks(TaskLoggingHelper log, IBuildContext context) + { + _log = log; + _context = context; + } + + /// + /// Verifies, that the specified is a valid file. + /// If a non-existing file shall be treated as an error, the corresponding parameter + /// must be set to true and the check will fail. The result of the check will be stored in the + /// parameter. + /// + /// The files to check. + /// If set to true, the check will fail, if the file does not exist. + /// The result of any previous check. If true is supplied, + /// the result can remain true. Otherwise the result will always be false, even, if the check succeeds. + internal void VerifyFilePresent(string fileSystemReference, bool treatAsError, ref bool checkResult) + { + checkResult = checkResult && LogCheckIfFileSystemEntryIsMissing( + _context.EnsureFileExists, + fileSystemReference, + treatAsError, + "The following file is required, but does not exist: {0}", + fileSystemReference); + } + + /// + /// Verifies, that the specified are valid files. + /// If a non-existing file shall be treated as an error, the corresponding parameter + /// must be set to true and the check will fail. The result of the check will be stored in the + /// parameter. + /// + /// The files to check. + /// If set to true, the check will fail, if at least one file does not exist. + /// The result of any previous check. If true is supplied, + /// the result can remain true. Otherwise the result will always be false, even, if the check succeeds. + internal void VerifyFilesPresent(IReadOnlyCollection fileSystemReferences, bool treatAsError, ref bool checkResult) + { + foreach (var possibleFileSystemEntry in fileSystemReferences) + { + VerifyFilePresent(possibleFileSystemEntry, treatAsError, ref checkResult); + } + } + + /// + /// Verifies, that the specified is a valid directory. + /// If a non-existing file shall be treated as an error, the corresponding parameter + /// must be set to true and the check will fail. The result of the check will be stored in the + /// parameter. + /// + /// The directory to check. + /// If set to true, the check will fail, if the directory does not exist. + /// The result of any previous check. If true is supplied, + /// the result can remain true. Otherwise the result will always be false, even, if the check succeeds. + internal void VerifyDirectoryPresent(string fileSystemReference, bool treatAsError, ref bool checkResult) + { + checkResult = checkResult && LogCheckIfFileSystemEntryIsMissing( + _context.EnsureDirectoryExists, + fileSystemReference, + treatAsError, + "The following file is required, but does not exist: {0}", + fileSystemReference); + } + + /// + /// Verifies, that the specified are valid directories. + /// If a non-existing file shall be treated as an error, the corresponding parameter + /// must be set to true and the check will fail. The result of the check will be stored in the + /// parameter. + /// + /// The directories to check. + /// If set to true, the check will fail, if at least one directory does not exist. + /// The result of any previous check. If true is supplied, + /// the result can remain true. Otherwise the result will always be false, even, if the check succeeds. + internal void VerifyDirectoriesPresent(IReadOnlyCollection fileSystemReferences, bool treatAsError, ref bool checkResult) + { + foreach (var possibleFileSystemEntry in fileSystemReferences) + { + VerifyDirectoryPresent(possibleFileSystemEntry, treatAsError, ref checkResult); + } + } + + + /// + /// Performs the specified method using the . + /// If the check fails, the specified will be issued to the log. + /// If no error is issued, the method will return true. + /// + /// The check to apply. + /// The file system entry to check. + /// If set to true, the will be issued as error; else a warning shall be submitted. + /// The message to log. + /// Formatting arguments for the . + /// true, if the check is issued no error; false otherwise. + private bool LogCheckIfFileSystemEntryIsMissing(Func performCheck, string fileSystemEntry, bool treatAsError, string message, params object[] args) + { + var flag = performCheck(fileSystemEntry); + if (!flag) + { + WriteMessageToLog(treatAsError, message, args); + } + + return !treatAsError || flag; + } + + /// + /// Writes a message to the log referenced by this instance. + /// + /// If set to true, the message will be treated as an error. A warning will be issued otherwise. + /// The message to supply. + /// The arguments to format the message. + private void WriteMessageToLog(bool treatAsError, string message, params object[] args) + { + Action logger = _log.LogWarning; + if (treatAsError) + { + logger = _log.LogError; + } + + logger(message, args); + } +} diff --git a/src/dscom.build/IBuildContext.cs b/src/dscom.build/IBuildContext.cs new file mode 100644 index 00000000..62b2b172 --- /dev/null +++ b/src/dscom.build/IBuildContext.cs @@ -0,0 +1,64 @@ +// Copyright 2022 dSPACE GmbH, Carsten Igel and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Build.Utilities; + +namespace dSPACE.Runtime.InteropServices.BuildTasks; + +/// +/// Interface representing the execution and build context. +/// This interface is used to separate task execution from +/// tlb conversion. +/// Hence implementations will perform any TLB conversion. +/// +public interface IBuildContext +{ + /// + /// Gets a value indicating whether the current run-time is windows-based or not. + /// + bool IsRunningOnWindows { get; } + + /// + /// Gets a verbatim description of the current run-time. + /// + string RuntimeDescription { get; } + + /// + /// When implemented it will return, whether the specified + /// points to a valid file or not. + /// + /// The name and path of the file. + /// true, if the file exists; false otherwise. + bool EnsureFileExists(string? fileNameAndPath); + + /// + /// When implemented it will return, whether the specified + /// points to a valid directory or not. + /// + /// The name and path of the directory. + /// true, if the directory exists; false otherwise. + bool EnsureDirectoryExists(string? directoryPath); + + /// + /// When implemented in a derived class, the conversion will take place + /// trying to load the assembly specified in the + /// of the specified object and convert it to the type library specified in + /// the of the same parameter. + /// Errors, warnings and conversion messages will be written to the build . + /// + /// The conversion settings to apply to the built-in converter. + /// The log to write error messages to. + /// true, if the conversion has taken place successfully; false otherwise. + bool ConvertAssemblyToTypeLib(TypeLibConverterSettings settings, TaskLoggingHelper log); +} diff --git a/src/dscom.build/LoggingTypeLibExporterSink.cs b/src/dscom.build/LoggingTypeLibExporterSink.cs new file mode 100644 index 00000000..d544853b --- /dev/null +++ b/src/dscom.build/LoggingTypeLibExporterSink.cs @@ -0,0 +1,61 @@ +// Copyright 2022 dSPACE GmbH, Carsten Igel and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace dSPACE.Runtime.InteropServices.BuildTasks; + +/// +/// Implementation of the a +/// forwarding all messages to the MsBuild log. +/// +internal sealed class LoggingTypeLibExporterSink : ITypeLibExporterNotifySink +{ + /// + /// The logging sink. + /// + private readonly TaskLoggingHelper _log; + + /// + /// Creates a new instance of the + /// using the specified as logging target. + /// + /// The log to write to. + internal LoggingTypeLibExporterSink(TaskLoggingHelper log) + { + _log = log; + } + + /// + void ITypeLibExporterNotifySink.ReportEvent(ExporterEventKind eventKind, int eventCode, string eventMsg) + { + var importance = eventKind switch + { + ExporterEventKind.NOTIF_TYPECONVERTED => MessageImportance.Low, + ExporterEventKind.NOTIF_CONVERTWARNING => MessageImportance.Normal, + ExporterEventKind.ERROR_REFTOINVALIDASSEMBLY => MessageImportance.High, + _ => MessageImportance.High, + }; + + _log.LogMessage(importance, "Received {0} event. Event Code is {1}: {2}", Enum.GetName(typeof(ExporterEventKind), eventKind), eventCode, eventMsg); + } + + /// + object? ITypeLibExporterNotifySink.ResolveRef(Assembly assembly) + { + return default; + } +} diff --git a/src/dscom.build/TlbExport.cs b/src/dscom.build/TlbExport.cs new file mode 100644 index 00000000..33b145cd --- /dev/null +++ b/src/dscom.build/TlbExport.cs @@ -0,0 +1,237 @@ +// Copyright 2022 dSPACE GmbH, Carsten Igel and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if NET5_0_OR_GREATER +using System.Runtime.Loader; +#else +using System.Reflection; +#endif + +using Microsoft.Build.Framework; + +namespace dSPACE.Runtime.InteropServices.BuildTasks; + +/// +/// Represents the task to export a type library file from an assembly. +/// +public sealed class TlbExport : Microsoft.Build.Utilities.Task +{ + /// + /// The build context applied to this instance. + /// + private readonly IBuildContext _context; + + /// + /// Creates a new instance of the + /// export class using the specified build context. + /// + /// The build context to apply. + public TlbExport(IBuildContext context) + { + _context = context; + } + + /// + /// Creates a new instance of the + /// class using the default build context. + /// + public TlbExport() : this(new DefaultBuildContext()) + { + } + + /// + /// Gets or sets the COM Type Library unique id. If this value + /// will be equal to , the id of the + /// resulting COM type library will be not be changed. + /// + public string TlbOverriddenId { get; set; } = Guid.Empty.ToString(); + + /// + /// Gets or sets the name of the resulting COM type library file (tlb). + /// This value must be set for this task to work and cannot be left empty. + /// Existing files will be overridden. + /// + [Required] + public string TargetFile { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the incoming managed assembly file (dll or exe). + /// This value must be set for this task to work and cannot be left empty. + /// The file must exist and be readable. + /// + [Required] + public string SourceAssemblyFile { get; set; } = string.Empty; + + /// + /// Gets or sets the additional type library files required for conversion. + /// + public ITaskItem[] TypeLibraryReferences { get; set; } = Array.Empty(); + + /// + /// Gets or sets the additional type library search paths required for conversion. + /// + public ITaskItem[] TypeLibraryReferencePaths { get; set; } = Array.Empty(); + + /// + /// Gets or sets the additional assemblies that might be required for conversion. + /// + public ITaskItem[] AssemblyPaths { get; set; } = Array.Empty(); + + /// + public override bool Execute() + { + var targetAssemblyFile = GetTargetRuntimeAssembly(); + + // if the assembly is found next to this assembly + if (File.Exists(targetAssemblyFile)) + { +#if NET5_0_OR_GREATER + // Load the assembly from a context + var loadContext = new AssemblyLoadContext($"msbuild-preload-ctx-{Guid.NewGuid()}", true); + + try + { + _ = loadContext.LoadFromAssemblyPath(targetAssemblyFile); + + // Execute the task with the resolved assembly. + return ExecuteTask(); + } + finally + { + // unload the assembly + loadContext.Unload(); + } +#else + _ = Assembly.LoadFrom(targetAssemblyFile); + + return ExecuteTask(); +#endif + } + else + { + // Make .NET Runtime resolve the file itself + return ExecuteTask(); + } + } + + /// + /// Performs the real task execution with the maybe temporarily loaded + /// Interop Assembly. + /// + /// The result of the task. + private bool ExecuteTask() + { + if (!_context.IsRunningOnWindows) + { + var verbatimDescription = _context.RuntimeDescription; + Log.LogError("This task can only be executed on Microsoft Windows (TM) based operating systems. This platform does not support the creation of this task: {0}", verbatimDescription); + return false; + } + + if (!Guid.TryParse(TlbOverriddenId, out var tlbOverriddenId)) + { + Log.LogError("Cannot convert {0} to a valid Guid", TlbOverriddenId); + return false; + } + + // Create type library converter settings from task parameters + var settings = new TypeLibConverterSettings() + { + Out = TargetFile, + Assembly = SourceAssemblyFile, + OverrideTlbId = tlbOverriddenId, + TLBReference = ConvertTaskItemToFsPath(TypeLibraryReferences, false), + TLBRefpath = ConvertTaskItemToFsPath(TypeLibraryReferencePaths, false), + ASMPath = ConvertTaskItemToFsPath(AssemblyPaths, true) + }; + + // Issue a warning, if the type library is about to be overridden. + if (settings.OverrideTlbId != Guid.Empty) + { + Log.LogMessage(MessageImportance.High, "The default unique id of the resulting type library will be overridden with the following value: {0}", settings.OverrideTlbId); + } + + // Perform file system checks. + var checks = new FileSystemChecks(Log, _context); + + var result = true; + checks.VerifyFilePresent(settings.Assembly, true, ref result); + checks.VerifyFilesPresent(settings.TLBReference, false, ref result); + checks.VerifyDirectoriesPresent(settings.TLBRefpath, false, ref result); + checks.VerifyFilesPresent(settings.ASMPath, false, ref result); + + // run conversion, if result has been successful. + result = result && _context.ConvertAssemblyToTypeLib(settings, Log); + + // report success or failure. + return result && !Log.HasLoggedErrors; + } + + /// + /// Takes the of the specified + /// and interprets them as file system entry and hence a file or directory path. + /// + /// The items to interpret a file system entries. + /// If set to true, the can + /// contain the Metadata value 'HintPath'. Hence the data will be scanned for this. + /// If not, it is assumed, that the property + /// contains the file system path. + /// The converted item-spec values. + private static string[] ConvertTaskItemToFsPath(IReadOnlyCollection items, bool canContainHintPaths) + { + const string HintPath = nameof(HintPath); + + var itemsWithHintPath = Enumerable.Empty(); + if (canContainHintPaths) + { + itemsWithHintPath = items + .Where(item => item != null) + .Where(item => !string.IsNullOrWhiteSpace(item.GetMetadata(HintPath))); + } + + var remainingItems = items.Except(itemsWithHintPath); + + return itemsWithHintPath + .Select(item => item.GetMetadata(HintPath)) + .Union(remainingItems.Where(item => item != null) + .Select(item => item.ItemSpec)).ToArray(); + } + + + /// + /// Gets the path to the path to the dSPACE.Runtime.InteropServices.dll + /// next to this assembly. + /// + /// The assembly file path. + private static string GetTargetRuntimeAssembly() + { + var assemblyPath = typeof(TlbExport).Assembly.Location; + var extension = Path.GetExtension(assemblyPath); + var fileBaseName = typeof(TlbExport).Namespace; + fileBaseName = Path.GetFileNameWithoutExtension(fileBaseName); + var fileName = fileBaseName + extension; + var assemblyDir = Path.GetDirectoryName(assemblyPath); + + var targetAssemblyFile = fileName; + + if (!string.IsNullOrWhiteSpace(assemblyDir)) + { + targetAssemblyFile = Path.Combine(assemblyDir, fileName); + } + + return targetAssemblyFile; + } + + +} diff --git a/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Task.targets b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Task.targets new file mode 100644 index 00000000..8537178c --- /dev/null +++ b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Task.targets @@ -0,0 +1,50 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + <_DsComTargetFramework>net6.0 + <_DsComTargetFramework Condition="'$(TargetFramework)' == 'net48'">$(TargetFramework) + + <_DsComTaskAssemblyFile>$(MsBuildThisFileDirectory)_dscom\$(_DsComTargetFramework)\dSPACE.Runtime.InteropServices.BuildTasks.dll + + + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets new file mode 100644 index 00000000..d82b5ba9 --- /dev/null +++ b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets @@ -0,0 +1,69 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + <_DsComToolsFileDir>$(MsBuildThisFileDirectory)..\tools\x64\ + <_DsComToolsFileDir Condition="'$(Platform)' == 'x86' OR ('$(Platform)' == 'AnyCPU' AND '$(SolutionPlatform)' == 'x86')">$(MsBuildThisFileDirectory)..\tools\x86\ + + + + + + <_DsComTlbExportAssemblyPathsWithHintPath Include="@(DsComTlbExportAssemblyPaths->HasMetadata('HintPath'))" /> + <_DsComTlbExportAssemblyPathsWithoutHintPath Include="@(DsComTlbExportAssemblyPaths)" /> + <_DsComTlbExportAssemblyPathsWithoutHintPath Remove="@(_DsComTlbExportAssemblyPathsWithHintPath)" /> + + + + <_DsComTypeLibraryReferences Condition="@(DsComTlbExportTlbReferences->Count()) > 0">--tlbreference "@(DsComTlbExportTlbReferences, '" --tlbreference "')" + <_DsComTypeLibraryReferencePaths Condition="@(DsComTlbExportReferencePaths->Count()) > 0">--tlbrefpath "@(DsComTlbExportReferencePaths, '"--tlbrefpath "')" + <_DsComAssemblyReferences Condition="@(_DsComTlbExportAssemblyPathsWithoutHintPath->Count()) > 0">--asmpath "@(_DsComTlbExportAssemblyPathsWithoutHintPath, '"--asmpath "')" + <_DsComAssemblyReferences Condition="@(_DsComTlbExportAssemblyPathsWithHintPath->Count()) > 0">--asmpath "@(_DsComTlbExportAssemblyPathsWithHintPath->GetMetadata('HintPath'), '"--asmpath "')" + <_DsComArguments>tlbexport + <_DsComArguments Condition="'$(DsComTypeLibraryUniqueId)' != '' AND '$(DsComTypeLibraryUniqueId)' != '$([System.Guid]::Empty.ToString())'">$(_DsComArguments) --overridetlbid "$(DsComTypeLibraryUniqueId)" + <_DsComArguments Condition="'$(DsComToolVerbose)' == 'true'"> $(_DsComArguments) --verbose + <_DsComArguments> $(_DsComArguments) $(_DsComAssemblyReferences) + <_DsComArguments> $(_DsComArguments) $(_DsComTypeLibraryReferences) + <_DsComArguments> $(_DsComArguments) $(_DsComTypeLibraryReferencePaths) + <_DsComArguments> $(_DsComArguments) $(_DsComExportTypeLibraryAssemblyFile) + <_DsComArguments> $(_DsComArguments) --out $(_DsComExportTypeLibraryTargetFile) + + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.props b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.props new file mode 100644 index 00000000..5c55c979 --- /dev/null +++ b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.props @@ -0,0 +1,46 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + <_DsComTlbExt>.tlb + + <_DsComForceToolUsage>true + + + + + 00000000-0000-0000-0000-000000000000 + + false + + true + + false + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.targets b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.targets new file mode 100644 index 00000000..1fbac616 --- /dev/null +++ b/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.targets @@ -0,0 +1,82 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + <_DsComExporterTargetsFile>$(MsBuildThisFileDirectory)dSPACE.Runtime.InteropServices.BuildTasks.Task.targets + <_DsComExporterTargetsFile Condition="'$(TargetFramework)' == 'netstandard2.0' OR ('$(TargetFrameworks)' != '' AND $(TargetFrameworks.Contains('netstandard2.0'))) OR 'Platform' == 'x86' OR ('Platform' == 'AnyCPU' AND '$(SolutionPlatform)' == 'x86')">$(MsBuildThisFileDirectory)dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets + <_DsComExporterTargetsFile Condition="'$(_DsComForceToolUsage)' == 'true'">$(MsBuildThisFileDirectory)dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets + + + + + + <_DsComExportTypeLibraryTargetFile>$(TargetDir)\$(TargetName)$(_DsComTlbExt) + + <_DsComExportTypeLibraryAssemblyFile>$(TargetPath) + + + + + <_DsComFromReferencesWithoutHintPath Include="@(References)" Exclude="@(References->HasMetadata('HintPath'))"/> + + <_DsComFromReferencesWithEmptyHintPath Include="@(References->HasMetadata('HintPath')->WithMetadata('HintPath', ''))"/> + + + + + <_DsComReferences Include="@(_DsComFromReferencesWithEmptyHintPath)" Condition="'$(DsComTlbExportIncludeReferencesWithoutHintPath)' == 'true'" /> + + <_DsComReferences Include="@(_DsComFromReferencesWithoutHintPath)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/doc/ReadMe.md b/src/dscom.build/doc/ReadMe.md new file mode 100644 index 00000000..115c8f29 --- /dev/null +++ b/src/dscom.build/doc/ReadMe.md @@ -0,0 +1,253 @@ +# dscom.build + +## Project + +This project consumes the core tasks package of MsBuild and the dSPACE.Runtime.InteropServices assembly. +The resulting assembly will be called `dSPACE.Runtime.InteropServices.BuildTasks`. This will also be the name of the NuGet package. + +The project consists of two notable classes. + +### Class `TlbExport` + +The class [`dSPACE.Runtime.InteropServices.Build.TlbExport`](/src/dscom.build/TlbExport.cs) contains the implementation of the build task. + +It consumes the following properties: + +* `TlbOverriddenId`: The id of the type library. If it is set to the empty Guid, the Guid of the TypeLibrary will be used. +* `TargetFile` (Required): The full path to the type library to export. +* `SourceAssemblyFile` (Required): The full path of the managed assembly to export. +* `TypeLibraryReferences`: The input type libraries to reference during export. +* `TypeLibraryReferencePaths`: A list of paths containing type library files to reference automatically during export. +* `AssemblyPaths`: The references of the source assembly which must be loaded for export. + +It contains the method `bool Execute()` as an implementation of the `ITask` interface. + +The execute method will load the `dSPACE.Runtime.InteropServices.dll`. +It tries to locate the file next to the build task assembly and - if cannot be present at that time - let the .NET runtime try to resolve it. + +During the execution phase, the following steps will be made: + +* Parse the overridden type library Guid. +* Fail execution, if the build is not executed on windows. +* Convert the `ITaskItems` (cf. `TypeLibraryReferences`, `TypeLibraryReferencePaths`, `AssemblyPaths`) to file system identifiers. +If a file or directory is not present, a warning will be issued using the MsBuild `TaskLoggingHelper`. +The export will continue. +* Resolve the `SourceAssemblyFile` and issue an error, if it was not found. +* Convert the assembly using the `DefaultBuildContext`. + +The `DefaultBuildContext` represents a level of abstraction. +It contains the part of the implementation that cannot be stubbed or mocked at run-time. + +The execution will be successful, if the build context returns success and the task logging helper did not log any errors. + +### Class `DefaultBuildContext` + +The `DefaultBuildContext` is an implementation of the [`IBuildContext`](/src/dscom.build/IBuildContext.cs) interface. +It provides the following operations: + +* Check, whether the build is running on windows. +* Check, whether given files and directories exist. +* Perform the export using the `TypeLibConverterSettings`. + +The export is done using the `TypeLibConverter` instance and an implementation of the [`ITypeLibExporterNotifySink`](/src/dscom.build/LoggingTypeLibExporterSink.cs), which logs the events to the build log. + +If the type library converter did not report a successful export, an error will be issued. If the expected file did not exists after the export, a warning shall be issued. + +It is an issue for discussion, if this check can be added to the `TlbExport` implementation. This way, the warning can be unit tested. + +### Build sequence (TlbExport) + +```ditaa + ┌───────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────────┐ ┌────────┐ ┌───┐ + │BuildEngine│ │TlbExport│ │IBuildContext│ │AppDomain.Current│ │Assembly│ │Log│ + └─────┬─────┘ └────┬────┘ └──────┬──────┘ └────────┬────────┘ └───┬────┘ └─┬─┘ + │ Execute ┌┴┐ │ │ │ │ + │────────────────────────────────>│ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ IsRunningOnWindows() │ │ │ │ + │ │ │ ──────────────────────────────────────────────────────────────────────────────>│ │ │ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + ╔══════╤════════╪═════════════════════════════════╪═╪════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════════════════════════════════════════════════════════════════╪═════════════════════╗ + ║ ALT │ result true │ │ │ │ │ │ ║ + ╟──────┘ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(TypeLibraryReferences, SupportHintPath=False) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(TypeLibraryReferencePaths, SupportHintPath=False) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(AssemblyPaths, SupportHintPath=True) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ set_TLBReference(conversionResult[TypeLibraryReferences]) │ ┌────────────────────────┐ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────>│TypeLibConverterSettings│ │ │ │ ║ + ║ │ │ │ │ └───────────┬────────────┘ │ │ │ ║ + ║ │ │ │ set_TLBRefpath(conversionResult[TypeLibraryReferencePaths]) │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ set_ASMPath(conversionResult[AssemblyPaths]) │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyFilesPresent(Assembly, ReportAsError=True) │ ┌────────────────┐ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│FileSystemChecks│ │ │ │ ║ + ║ │ │ │ │ │ └───────┬────────┘ │ │ │ ║ + ║ │ │ │ │ VerifyFilesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyFilesPresent(TypeLibraryReferences, AssemblyPaths, ReportAsError=False)│ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ VerifyFilesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyDirectoriesPresent(TypeLibraryReferencePaths, ReportAsError=False) │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ VerifyDirectoriesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ConvertAssemblyToTypeLib ┌┴┐ │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────>│ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═══════════════════════════════════════════════════════════╪══════════════════╗ │ │ ║ + ║ │ │ │ ║ ALT │ IsNet50OrLater │ │ │ │ ║ │ │ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ add_Resolving(ResolveAssemblyFromSettings) ┌───────────────────┐ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────>│AssemblyLoadContext│ │ ║ │ │ ║ + ║ │ │ │ ╠═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═══════════════════════════════════════════════════════════╪══════════════════╣ │ │ ║ + ║ │ │ │ ║ [IsNetFx] │ │ │ │ │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ │ add_AssemblyResolve(ResolveAssemblyFromSettings) │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ │ │ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪══════════════════╝ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ Load(TypeLibExportSettings.SourceAssembly) │ │ ╔═══════════════════════════╗ │ ║ + ║ │ │ │ │ │ ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> ║Load Assemblies recursive ░║ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ╚═════════════════════════╤═╝ │ ║ + ║ │ │ │ │ │ │ │ set_Logger(log) │ │ │ ┌──────────────────────────┐ │ ║ + ║ │ │ │ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│LoggingTypeLibExporterSink│ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ └────────────┬─────────────┘ │ │ ║ + ║ │ │ │ │ │ │ ConvertAssemblyToTypeLib(Assembly, TypeLibConverterSettings, LoggingTypeLibExporterSink) │ │ ┌────────────────┐ │ ║ + ║ │ │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│TypeLibConverter│ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ └───────┬────────┘ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╗ ║ + ║ │ │ │ ║ ALT │ result is null │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ │ │ │ Error │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╝ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╗ ║ + ║ │ │ │ ║ ALT │ TLB Not Existing │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ │ │ │ Warning(DSCOM001) │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╝ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ ╔══════╤══════╪═╪═══════════════════════════════════════════════════════════════════════════════╪═╪════════════════════════════╪═══════════════════════════════╪════════════════════════════\╪/══════════════════╗ │ │ │ │ │ ║ + ║ │ ║ ALT │ IsNet50OrLater │ │ │ │ X ║ │ │ │ │ │ ║ + ║ │ ╟──────┘ │ │ │ │ │ │ /│\ ║ │ │ │ │ │ ║ + ║ │ ║ │ │ │ │ Unload │ │ │ ║ │ │ │ │ │ ║ + ║ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ │ │ │ │ │ ║ + ║ │ ╚═════════════╪═╪═══════════════════════════════════════════════════════════════════════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═══════════════════╝ │ │ │ │ │ ║ + ║ │ │ │ └┬┘ │ │ │ │ │ │ │ │ ║ + ║ │return result && Log.HasNoErrors │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │<────────────────────────────────│ │ │ │ │ │ │ │ │ │ │ ║ + ╠═══════════════╪═════════════════════════════════╪═╪════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═════════════════════╣ + ║ [result false]│ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ Error │ │ │ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ + ║ │ └┬┘ │ │ │ │ │ │ │ │ │ ║ + ║ │ false │ │ │ │ │ │ │ │ │ │ ║ + ║ │<─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ │ │ │ │ │ │ │ │ ║ + ╚═══════════════╪══════════════════════════════════╪═════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═════════════════════╝ + ┌─────┴─────┐ ┌────┴────┐ ┌──────┴──────┐ ┌───────────┴────────────┐ ┌───────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐ ┌───┴────┐ ┌────────────┴─────────────┐ ┌───────┴────────┐ ┌─┴─┐ + │BuildEngine│ │TlbExport│ │IBuildContext│ │TypeLibConverterSettings│ │FileSystemChecks│ │AssemblyLoadContext│ │AppDomain.Current│ │Assembly│ │LoggingTypeLibExporterSink│ │TypeLibConverter│ │Log│ + └───────────┘ └─────────┘ └─────────────┘ └────────────────────────┘ └────────────────┘ └───────────────────┘ └─────────────────┘ └────────┘ └──────────────────────────┘ └────────────────┘ └───┘ +``` + +## Test + +### Unit tests + +The class `TlbExport` is written with a level of abstraction and hence is tested with a [unit test](/src/dscom.test/tests/BuildTaskTest.cs). + +### Acceptance tests + +The examples [32-bit](/examples/32bit/) and [outproc](/examples/outproc/) each provide an `msbuild-acceptance-test.cmd` script. +This file builds and packages the build task and adds it to the `.csproj` file of the resulting assembly. +Then the build is conducted. It must be warning free. + +## Packaging + +The packaging will contain the executable file for [dscom CLI](/src/dscom.client/dscom.client.csproj) for both platforms, x64 and x86. The configuration is Release. +The executable will be published in Release as a framework dependent deployment using a [single file](https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli), which means that [dSPACE.Runtime.InteropServices](/src/dscom/dscom.csproj) and [System.CommandLine](https://packages.nuget.org/packages/System.CommandLine/) are bundled, but no AOT takes place and no framework files will be part of the bundle. + +The packaging takes place in [DsComPackaging.targets](/src/dscom.build/DsComPackaging.targets) which is imported by [dscom.build.csproj](/src/dscom.build/dscom.build.csproj). +The targets file hooks into the packaging procedure by enforcing to be run before the `GenerateNuspec` target of the NuGet Client which is part of the packaging process by the MsBuild dotnet sdk in the [NuGet.Build.Tasks.Pack.targets](https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Build.Tasks.Pack/NuGet.Build.Tasks.Pack.targets) and exposes the `GenerateNuspecDependsOn` array variable. +Two targets must be executed before generating the NuSpec file: + +**DsComBuildPackTaskDependencies**: + This task packs the [dSPACE.Runtime.InteropServices](/src/dscom/dscom.csproj) and [System.CommandLine](https://packages.nuget.org/packages/System.CommandLine/) assembly into the package. This will enable the [TlbExport](/src/dscom.build/TlbExport.cs) task to load the assembly. + Due to a [bug in MsBuild](https://github.com/dotnet/msbuild/issues/1755) and another [issue with NuGet](https://github.com/NuGet/Home/issues/4704), MsBuild does not add assemblies from dependent package to the resolve handler. + Hence the dependent assembly must be bundled with the NuGet package. Refer to a [blog article](https://natemcmaster.com/blog/2017/11/11/msbuild-task-with-dependencies/) for details. + +**DsComBuildPackTool**: + The MsBuild process is hosted using the target framework, i.e. .NET FullFramework 4.8 or .NET 6.0 using an x64 based process environment. + In order to enable the build using the 32bit builder and other target frameworks, the executables will be added to the NuGet package. + This approach has been inspired by [Grpc.Tools](https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props) NuGet package which bundles the `protoc.exe` and `grpc_cpp_plugin.exe` executables. + In order to get the matching binaries without interfering with the regular MsBuild and NuGet packaging, the task calls the [MsBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-task) task for the [dscom.client.csproj](/src/dscom.client/dscom.client.csproj) using the targets `Restore`, `Build` and `Pack` targets for both platforms, x86 and x64 using a different output directory. + +## Targets and Properties + +The package containing the build task comes with [props and targets for MsBuild](https://learn.microsoft.com/en-us/nuget/concepts/msbuild-props-and-targets). +These files are automatically included to the build using the naming convention. +There is also a folder for .NET Standard 2.0 which will only include the general files. + +**[dSPACE.Runtime.InteropServices.BuildTasks.props](/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.props)**: + This property file contains the general properties for the integration of the build task. + +**[dSPACE.Runtime.InteropServices.BuildTasks.targets](/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.targets)**: + This target file includes the general tasks and targets. + It will also create the properties and item groups based on the data provided by the project being build. + Depending on the enforced usage of the tools or task, it will include the tool or task specific targets fill. + +**[dSPACE.Runtime.InteropServices.BuildTasks.Task.targets](/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Task.targets)**: + This target file includes the targets and tasks which invoke the TLB export as MsBuild task. + +**[dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets](/src/dscom.build/dSPACE.Runtime.InteropServices.BuildTasks.Tools.targets)**: + This target file includes the targets and tasks which invoke the TLB export a call to the bundled executables. + +The general approach is designed by the MsBuild SDK based projects. +The props file will be included in the beginning of the project file _automatically_, i.e. before the project file is read. +The targets file will be in included at the end of the project file _automatically_, i.e. after the project file has been read. +Hence project specific settings are only available in the targets file. + +## Limitations + +### InProc Export + +The [LibraryWriter](/src/dscom/writer/LibraryWriter.cs) currently refuses to write the type library file (TLB), if the file is hosted within the MsBuild Process. +The reason is currently unknown. +Therefore it is not recommended to set `_DsComForceToolUsage` to `false`. + +### .NET Standard 2.0 compliant NuGet package + +The `Pack` target currently does not create a .NET Standard 2.0 compliant NuGet package. +Therefore, when adding the `dSPACE.Runtime.InteropServices.Build` package to a .NET Standard 2.0 targeting project, the warning [NU1701](https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1701) will be issued. +This warning can be explicitly disabled for the package using the package meta data: + +```xml + + ... + + ... + +``` diff --git a/src/dscom.build/doc/TlbExport.sequence-ditaa.txt b/src/dscom.build/doc/TlbExport.sequence-ditaa.txt new file mode 100644 index 00000000..89741710 --- /dev/null +++ b/src/dscom.build/doc/TlbExport.sequence-ditaa.txt @@ -0,0 +1,113 @@ + ┌───────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────────┐ ┌────────┐ ┌───┐ + │BuildEngine│ │TlbExport│ │IBuildContext│ │AppDomain.Current│ │Assembly│ │Log│ + └─────┬─────┘ └────┬────┘ └──────┬──────┘ └────────┬────────┘ └───┬────┘ └─┬─┘ + │ Execute ┌┴┐ │ │ │ │ + │────────────────────────────────>│ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ IsRunningOnWindows() │ │ │ │ + │ │ │ ──────────────────────────────────────────────────────────────────────────────>│ │ │ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + ╔══════╤════════╪═════════════════════════════════╪═╪════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════════════════════════════════════════════════════════════════╪═════════════════════╗ + ║ ALT │ result true │ │ │ │ │ │ ║ + ╟──────┘ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(TypeLibraryReferences, SupportHintPath=False) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(TypeLibraryReferencePaths, SupportHintPath=False) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ ║ + ║ │ │ │────┐ │ │ │ │ ║ + ║ │ │ │ │ ConvertTaskItemToFsPath(AssemblyPaths, SupportHintPath=True) │ │ │ │ ║ + ║ │ │ │<───┘ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ set_TLBReference(conversionResult[TypeLibraryReferences]) │ ┌────────────────────────┐ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────>│TypeLibConverterSettings│ │ │ │ ║ + ║ │ │ │ │ └───────────┬────────────┘ │ │ │ ║ + ║ │ │ │ set_TLBRefpath(conversionResult[TypeLibraryReferencePaths]) │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ set_ASMPath(conversionResult[AssemblyPaths]) │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyFilesPresent(Assembly, ReportAsError=True) │ ┌────────────────┐ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│FileSystemChecks│ │ │ │ ║ + ║ │ │ │ │ │ └───────┬────────┘ │ │ │ ║ + ║ │ │ │ │ VerifyFilesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyFilesPresent(TypeLibraryReferences, AssemblyPaths, ReportAsError=False)│ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ VerifyFilesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ VerifyDirectoriesPresent(TypeLibraryReferencePaths, ReportAsError=False) │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ VerifyDirectoriesPresent(files) │ │ │ │ ║ + ║ │ │ │ │ <──────────────────────────────────────────────────────────── │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ConvertAssemblyToTypeLib ┌┴┐ │ │ │ │ │ ║ + ║ │ │ │ ─────────────────────────────────────────────────────────────────────────────>│ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═══════════════════════════════════════════════════════════╪══════════════════╗ │ │ ║ + ║ │ │ │ ║ ALT │ IsNet50OrLater │ │ │ │ ║ │ │ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ add_Resolving(ResolveAssemblyFromSettings) ┌───────────────────┐ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────>│AssemblyLoadContext│ │ ║ │ │ ║ + ║ │ │ │ ╠═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═══════════════════════════════════════════════════════════╪══════════════════╣ │ │ ║ + ║ │ │ │ ║ [IsNetFx] │ │ │ │ │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ │ add_AssemblyResolve(ResolveAssemblyFromSettings) │ │ ║ │ │ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ │ │ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪══════════════════╝ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ Load(TypeLibExportSettings.SourceAssembly) │ │ ╔═══════════════════════════╗ │ ║ + ║ │ │ │ │ │ ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────> ║Load Assemblies recursive ░║ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ ╚═════════════════════════╤═╝ │ ║ + ║ │ │ │ │ │ │ │ set_Logger(log) │ │ │ ┌──────────────────────────┐ │ ║ + ║ │ │ │ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│LoggingTypeLibExporterSink│ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ └────────────┬─────────────┘ │ │ ║ + ║ │ │ │ │ │ │ ConvertAssemblyToTypeLib(Assembly, TypeLibConverterSettings, LoggingTypeLibExporterSink) │ │ ┌────────────────┐ │ ║ + ║ │ │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│TypeLibConverter│ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ └───────┬────────┘ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╗ ║ + ║ │ │ │ ║ ALT │ result is null │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ │ │ │ Error │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╝ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ ╔══════╤════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╗ ║ + ║ │ │ │ ║ ALT │ TLB Not Existing │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ╟──────┘ │ │ │ │ │ │ │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ │ │ │ Warning(DSCOM001) │ │ │ │ ║ ║ + ║ │ │ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ ║ + ║ │ │ │ ╚═══════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═══════════╝ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ ╔══════╤══════╪═╪═══════════════════════════════════════════════════════════════════════════════╪═╪════════════════════════════╪═══════════════════════════════╪════════════════════════════\╪/══════════════════╗ │ │ │ │ │ ║ + ║ │ ║ ALT │ IsNet50OrLater │ │ │ │ X ║ │ │ │ │ │ ║ + ║ │ ╟──────┘ │ │ │ │ │ │ /│\ ║ │ │ │ │ │ ║ + ║ │ ║ │ │ │ │ Unload │ │ │ ║ │ │ │ │ │ ║ + ║ │ ║ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ │ │ │ │ │ ║ + ║ │ ╚═════════════╪═╪═══════════════════════════════════════════════════════════════════════════════╪═╪════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═══════════════════╝ │ │ │ │ │ ║ + ║ │ │ │ └┬┘ │ │ │ │ │ │ │ │ ║ + ║ │return result && Log.HasNoErrors │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │<────────────────────────────────│ │ │ │ │ │ │ │ │ │ │ ║ + ╠═══════════════╪═════════════════════════════════╪═╪════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═════════════════════╣ + ║ [result false]│ │ │ │ │ │ │ │ │ │ │ │ ║ + ║ │ │ │ │ │ │ Error │ │ │ │ │ │ ║ + ║ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>│ ║ + ║ │ └┬┘ │ │ │ │ │ │ │ │ │ ║ + ║ │ false │ │ │ │ │ │ │ │ │ │ ║ + ║ │<─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ │ │ │ │ │ │ │ │ ║ + ╚═══════════════╪══════════════════════════════════╪═════════════════════════════════════════════════════════════════════════════════╪═════════════════════════════╪═══════════════════════════════╪═════════════════════════════╪═════════════════════════════╪═══════════════════════╪════════════════════════════╪════════════════════════════════╪═════════════════════╪═════════════════════╝ + ┌─────┴─────┐ ┌────┴────┐ ┌──────┴──────┐ ┌───────────┴────────────┐ ┌───────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐ ┌───┴────┐ ┌────────────┴─────────────┐ ┌───────┴────────┐ ┌─┴─┐ + │BuildEngine│ │TlbExport│ │IBuildContext│ │TypeLibConverterSettings│ │FileSystemChecks│ │AssemblyLoadContext│ │AppDomain.Current│ │Assembly│ │LoggingTypeLibExporterSink│ │TypeLibConverter│ │Log│ + └───────────┘ └─────────┘ └─────────────┘ └────────────────────────┘ └────────────────┘ └───────────────────┘ └─────────────────┘ └────────┘ └──────────────────────────┘ └────────────────┘ └───┘ \ No newline at end of file diff --git a/src/dscom.build/doc/TlbExport.sequence.puml b/src/dscom.build/doc/TlbExport.sequence.puml new file mode 100644 index 00000000..b0dd80dc --- /dev/null +++ b/src/dscom.build/doc/TlbExport.sequence.puml @@ -0,0 +1,64 @@ +@startuml "TlbExport" +Participant BuildEngine as MsBuild + +MsBuild -> TlbExport: Execute +activate TlbExport + +TlbExport -> IBuildContext: IsRunningOnWindows() +alt result true + TlbExport -> TlbExport: ConvertTaskItemToFsPath(TypeLibraryReferences, SupportHintPath=False) + TlbExport -> TlbExport: ConvertTaskItemToFsPath(TypeLibraryReferencePaths, SupportHintPath=False) + TlbExport -> TlbExport: ConvertTaskItemToFsPath(AssemblyPaths, SupportHintPath=True) + + create TypeLibConverterSettings + + TlbExport -> TypeLibConverterSettings: set_TLBReference(conversionResult[TypeLibraryReferences]) + TlbExport -> TypeLibConverterSettings: set_TLBRefpath(conversionResult[TypeLibraryReferencePaths]) + TlbExport -> TypeLibConverterSettings: set_ASMPath(conversionResult[AssemblyPaths]) + + create FileSystemChecks + TlbExport -> FileSystemChecks: VerifyFilesPresent(Assembly, ReportAsError=True) + FileSystemChecks -> IBuildContext: VerifyFilesPresent(files) + TlbExport -> FileSystemChecks: VerifyFilesPresent(TypeLibraryReferences, AssemblyPaths, ReportAsError=False) + FileSystemChecks -> IBuildContext: VerifyFilesPresent(files) + TlbExport -> FileSystemChecks: VerifyDirectoriesPresent(TypeLibraryReferencePaths, ReportAsError=False) + FileSystemChecks -> IBuildContext: VerifyDirectoriesPresent(files) + + TlbExport -> IBuildContext: ConvertAssemblyToTypeLib + activate IBuildContext + alt IsNet50OrLater + create AssemblyLoadContext + IBuildContext -> AssemblyLoadContext: add_Resolving(ResolveAssemblyFromSettings) + else IsNetFx + IBuildContext -> AppDomain.Current: add_AssemblyResolve(ResolveAssemblyFromSettings) + end + + IBuildContext -> Assembly: Load(TypeLibExportSettings.SourceAssembly) + note right: Load Assemblies recursive + + create LoggingTypeLibExporterSink + IBuildContext -> LoggingTypeLibExporterSink: set_Logger(log) + + create TypeLibConverter + IBuildContext -> TypeLibConverter: ConvertAssemblyToTypeLib(Assembly, TypeLibConverterSettings, LoggingTypeLibExporterSink) + alt result is null + IBuildContext -> Log: Error + end + + alt TLB Not Existing + IBuildContext -> Log: Warning(DSCOM001) + end + + alt IsNet50OrLater + TlbExport -> AssemblyLoadContext: Unload + destroy AssemblyLoadContext + end + deactivate IBuildContext + + TlbExport -> MsBuild: return result && Log.HasNoErrors + +else result false + TlbExport -> Log: Error + return false +end +@enduml \ No newline at end of file diff --git a/src/dscom.build/dscom.build.csproj b/src/dscom.build/dscom.build.csproj new file mode 100644 index 00000000..72f7233c --- /dev/null +++ b/src/dscom.build/dscom.build.csproj @@ -0,0 +1,61 @@ + + + + net6.0 + dSPACE.Runtime.InteropServices.BuildTasks + $(DsComBuildTargetFrameWork);net48 + x64 + dSPACE.Runtime.InteropServices.BuildTasks normal + v + true + build\_dscom\ + true + true + true + false + true + snupkg + + + + dSPACE GmbH + dSPACE.Runtime.InteropServices.BuildTasks + dSPACE COM tools - MsBuild Tasks + dSPACE COM tools - A replacement for tlbexp.exe and TypeLibConverter.ConvertAssemblyToTypeLib + true + README.md + packageIcon.png + dSPACE.Runtime.InteropServices.BuildTasks + Apache-2.0 + https://github.com/dspace-group/dscom + true + com;tlb;ole;idl;tlbexp;interop;typelib;ConvertAssemblyToTypeLib;TypeLibConverter;MsBuild + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.props b/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.props new file mode 100644 index 00000000..8d39e6a8 --- /dev/null +++ b/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.props @@ -0,0 +1,23 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + \ No newline at end of file diff --git a/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.targets b/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.targets new file mode 100644 index 00000000..c4ab9b09 --- /dev/null +++ b/src/dscom.build/netstandard2.0/dSPACE.Runtime.InteropServices.BuildTasks.targets @@ -0,0 +1,23 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + \ No newline at end of file diff --git a/src/dscom.client/AssemblyResolver.cs b/src/dscom.client/AssemblyResolver.cs index b73e5f62..6c53ab7d 100644 --- a/src/dscom.client/AssemblyResolver.cs +++ b/src/dscom.client/AssemblyResolver.cs @@ -19,7 +19,7 @@ namespace dSPACE.Runtime.InteropServices; /// /// Uses the "ASMPath" option to handle the AppDomain.CurrentDomain.AssemblyResolve event and try to load the specified assemblies. /// -internal class AssemblyResolver : IDisposable +internal sealed class AssemblyResolver : IDisposable { private bool _disposedValue; @@ -54,7 +54,7 @@ internal AssemblyResolver(TypeLibConverterOptions options) return null; } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_disposedValue) { diff --git a/src/dscom.client/dscom.client.csproj b/src/dscom.client/dscom.client.csproj index 762b241f..4953f509 100644 --- a/src/dscom.client/dscom.client.csproj +++ b/src/dscom.client/dscom.client.csproj @@ -6,6 +6,7 @@ net6.0 AnyCPU AnyCPU;x86 + win-x64;win-x86 dSPACE.Runtime.InteropServices normal v diff --git a/src/dscom.test/dscom.test.csproj b/src/dscom.test/dscom.test.csproj index 17540452..98b70239 100644 --- a/src/dscom.test/dscom.test.csproj +++ b/src/dscom.test/dscom.test.csproj @@ -16,6 +16,7 @@ + @@ -26,10 +27,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + diff --git a/src/dscom.test/tests/BuildTaskTest.cs b/src/dscom.test/tests/BuildTaskTest.cs new file mode 100644 index 00000000..547e1d64 --- /dev/null +++ b/src/dscom.test/tests/BuildTaskTest.cs @@ -0,0 +1,783 @@ +// Copyright 2022 dSPACE GmbH, Mark Lechtermann, Matthias Nissen and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using dSPACE.Runtime.InteropServices.BuildTasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using Moq; + +namespace dSPACE.Runtime.InteropServices.Tests; + +public class BuildTaskTest : BaseTest +{ + private sealed class BuildContextStub : IBuildContext + { + public ISet ExistingFiles = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + public ISet ExistingDirectories = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + public bool ShouldSucceed { get; set; } = true; + + public bool IsRunningOnWindows { get; set; } = true; + + public string RuntimeDescription { get; set; } = "x-unit runner"; + + public bool ConvertAssemblyToTypeLib(TypeLibConverterSettings settings, TaskLoggingHelper log) + { + return ShouldSucceed; + } + + public bool EnsureFileExists(string? fileNameAndPath) + { + return !ExistingFiles.Any() || ExistingFiles.Contains(fileNameAndPath ?? string.Empty); + } + + public bool EnsureDirectoryExists(string? directoryNameAndPath) + { + return !ExistingDirectories.Any() || ExistingDirectories.Contains(directoryNameAndPath ?? string.Empty); + } + } + + private sealed class BuildEngineStub : IBuildEngine + { + public bool ContinueOnError => true; + + public int LineNumberOfTaskNode => 1; + + public int ColumnNumberOfTaskNode => 1; + + public string ProjectFileOfTaskNode => "demo.test.csproj"; + + public int NumberOfCustomLogEvents { get; private set; } + + public int NumberOfMessageLogEvents { get; private set; } + + public int NumberOfWarningLogEvents { get; private set; } + + public int NumberOfErrorLogEvents { get; private set; } + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + return true; + } + + public void LogCustomEvent(CustomBuildEventArgs e) + { + NumberOfCustomLogEvents++; + } + + public void LogErrorEvent(BuildErrorEventArgs e) + { + NumberOfErrorLogEvents++; + } + + public void LogMessageEvent(BuildMessageEventArgs e) + { + NumberOfMessageLogEvents++; + } + + public void LogWarningEvent(BuildWarningEventArgs e) + { + NumberOfWarningLogEvents++; + } + } + + private static ITaskItem GetTaskItem(string itemSpec, string? path = null, bool useHintPath = false) + { + var taskItemMock = new Mock(MockBehavior.Loose); + + taskItemMock.SetupGet(inst => inst.ItemSpec).Returns(itemSpec); + + if (useHintPath) + { + const string HintPath = nameof(HintPath); + + var hintPath = path ?? itemSpec; + + taskItemMock.Setup(inst => inst.GetMetadata(It.IsNotIn(new string[] { HintPath }, StringComparer.InvariantCulture))).Returns(string.Empty); + taskItemMock.Setup(inst => inst.GetMetadata(It.Is(HintPath, StringComparer.InvariantCulture))).Returns(hintPath); + + taskItemMock.SetupGet(inst => inst.MetadataCount).Returns(1); + taskItemMock.SetupGet(inst => inst.MetadataNames).Returns(new string[] { HintPath }); + } + else + { + taskItemMock.Setup(inst => inst.GetMetadata(It.IsAny())).Returns(string.Empty); + + taskItemMock.SetupGet(inst => inst.MetadataCount).Returns(0); + taskItemMock.SetupGet(inst => inst.MetadataNames).Returns(Array.Empty()); + } + + return taskItemMock.Object; + } + + private static BuildContextStub GetBuildContext() + { + return new BuildContextStub(); + } + + private static TlbExport GetBuildTask(out BuildContextStub context, bool shouldSucceed = true) + { + context = GetBuildContext(); + context.ShouldSucceed = shouldSucceed; + var classUnderTest = new TlbExport(context) + { + BuildEngine = new BuildEngineStub() + }; + return classUnderTest; + } + + [Fact] + public void TestDefaultSettingsSuccessful() + { + var task = GetBuildTask(out _); + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } + + [Fact] + public void TestDefaultSettingsFailingOnNonWindows() + { + var task = GetBuildTask(out var context); + context.IsRunningOnWindows = false; + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeTrue(); + } + + [Fact] + public void TestDefaultSettingsFailsOnTransformationError() + { + var task = GetBuildTask(out _, false); + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } + + [Fact] + public void TestGuidIsEmpty() + { + var task = GetBuildTask(out _); + task.TlbOverriddenId = string.Empty; + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeTrue(); + } + + [Theory] + [InlineData("03bc128f-0e54-44f0-9c3e-3fa1ed1.ceeb1")] + [InlineData("03bc128g0e5444f09c3e3fa1ed1ceeb1")] + [InlineData("03bc128f-0e54-44f0-9c3e-3fa1ed1ceeb1-abcd")] + [InlineData("dcba-03bc128f-0e54-44f0-9c3e-3fa1ed1ceeb1")] + public void TestGuidIsMalformed(string malformedGuid) + { + var task = GetBuildTask(out _); + task.TlbOverriddenId = malformedGuid; + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeTrue(); + } + + [Theory] + [InlineData("03bc128f-0e54-44f0-9c3e-3fa1ed1ceeb1")] + [InlineData("03bc128f0e5444f09c3e3fa1ed1ceeb1")] + [InlineData("00000000-0000-0000-0000-000000000000")] + public void TestGuidIsCorrect(string guid) + { + var task = GetBuildTask(out _); + task.TlbOverriddenId = guid; + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } + + [Fact] + public void TestAssemblyFileCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } + + [Fact] + public void TestAssemblyFileCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath + ".notExisting"); + + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeTrue(); + } + + [Fact] + public void TestAssemblyFileReferencesItemSpecCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(filePath); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(0, "No warning should be present"); + } + + [Fact] + public void TestAssemblyFileReferencesItemSpecCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(filePath); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestAssemblyFileReferencesHintPathCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(fileName, filePath, true); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(0, "No warning should be present"); + } + + [Fact] + public void TestAssemblyFileReferencesHintPathCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(fileName, filePath, true); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestAssemblyFileReferencesHybridCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var arg = (i % 2 == 0) ? ((ValueTuple)(fileName, filePath)) : ((ValueTuple)(filePath, null)); + var (fn, fp) = arg; + var taskItem = GetTaskItem(fn, fp!, i % 2 == 0); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(0, "No warning should be present"); + } + + [Fact] + public void TestAssemblyFileReferencesHybridCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"ReferencedAssembly{i}.dll"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var arg = (i % 2 == 0) ? ((ValueTuple)(fileName, filePath)) : ((ValueTuple)(filePath, null)); + var (fn, fp) = arg; + var taskItem = GetTaskItem(fn, fp!, i % 2 == 0); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.AssemblyPaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencesItemSpecCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(filePath); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(0, "No warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencesItemSpecCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(filePath); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencesHintPathCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(fileName, filePath, true); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(5, "Five warnings should be present, since TLBs cannot be referenced using HintPath"); + } + + [Fact] + public void TestypeLibraryReferencesHintPathCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var taskItem = GetTaskItem(fileName, filePath, true); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestypeLibraryReferencesHybridCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var arg = (i % 2 == 0) ? ((ValueTuple)(fileName, filePath)) : ((ValueTuple)(filePath, null)); + var (fn, fp) = arg; + var taskItem = GetTaskItem(fn, fp!, i % 2 == 0); + taskItems.Add(taskItem); + context.ExistingFiles.Add(filePath); + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(2, "Five warnings should be present, since TLBs cannot be referenced using HintPath"); + } + + [Fact] + public void TestTypeLibraryReferencesHybridCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var fileName = $"TypeLibrary{i}.tlb"; + var filePath = Path.Combine(Path.GetTempPath(), fileName); + var arg = (i % 2 == 0) ? ((ValueTuple)(fileName, filePath)) : ((ValueTuple)(filePath, null)); + var (fn, fp) = arg; + var taskItem = GetTaskItem(fn, fp!, i % 2 == 0); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingFiles.Add(filePath); + } + } + + task.TypeLibraryReferences = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencePathsItemSpecCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var taskItem = GetTaskItem(dirPath); + taskItems.Add(taskItem); + context.ExistingDirectories.Add(dirPath); + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(0, "No warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencePathsItemSpecCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var taskItem = GetTaskItem(dirPath); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingDirectories.Add(dirPath); + } + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestTypeLibraryReferencePathsHintPathCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var taskItem = GetTaskItem(dirName, dirPath, true); + taskItems.Add(taskItem); + context.ExistingDirectories.Add(dirPath); + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(5, "Five warnings should be present, since TLBs cannot be referenced using HintPath"); + } + + [Fact] + public void TestypeLibraryReferencePathsHintPathCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var taskItem = GetTaskItem(dirName, dirPath, true); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingDirectories.Add(dirPath); + } + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestypeLibraryReferencePathsHybridCheckSuccess() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var arg = (i % 2 == 0) ? ((ValueTuple)(dirName, dirPath)) : ((ValueTuple)(dirPath, null)); + var (dn, dp) = arg; + var taskItem = GetTaskItem(dn, dp!, i % 2 == 0); + taskItems.Add(taskItem); + context.ExistingDirectories.Add(dirPath); + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().NumberOfWarningLogEvents.Should().Be(2, "Five warnings should be present, since TLBs cannot be referenced using HintPath"); + } + + [Fact] + public void TestTypeLibraryReferencePathsHybridCheckFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + var notFoundByRandom = new Random().Next(5) + 1; + + var taskItems = new List(); + for (var i = 1; i <= 5; i++) + { + var dirName = $"TypeLibrarySearchFolder{i}"; + var dirPath = Path.Combine(Path.GetTempPath(), dirName); + var arg = (i % 2 == 0) ? ((ValueTuple)(dirName, dirPath)) : ((ValueTuple)(dirPath, null)); + var (dn, dp) = arg; + var taskItem = GetTaskItem(dn, dp!, i % 2 == 0); + taskItems.Add(taskItem); + if (i != notFoundByRandom) + { + context.ExistingDirectories.Add(dirPath); + } + } + + task.TypeLibraryReferencePaths = taskItems.ToArray(); + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + task.BuildEngine.As().Should().NotBe(0, "At least one warning should be present"); + } + + [Fact] + public void TestBuildIsSuccessful() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + context.ShouldSucceed = true; + + task.Execute().Should().BeTrue(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } + + [Fact] + public void TestBuildIsSuccessFail() + { + var task = GetBuildTask(out var context); + var assemblyFileName = "MyAssemblyFile.dll"; + var assemblyFilePath = Path.Combine(Path.GetTempPath(), assemblyFileName); + task.SourceAssemblyFile = assemblyFilePath; + context.ExistingFiles.Add(assemblyFilePath); + + context.ShouldSucceed = false; + + task.Execute().Should().BeFalse(); + task.Log.HasLoggedErrors.Should().BeFalse(); + } +} +