{"id":385557,"date":"2024-06-29T06:10:50","date_gmt":"2024-06-29T06:10:50","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=385557"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=385557","title":{"rendered":"<span>Creating a NuGet package for a library with platform-specific API<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><em>It&#8217;s the English version of <\/em><a href=\"https:\/\/habr.com\/ru\/post\/564476\" rel=\"noopener noreferrer nofollow\"><em>this<\/em><\/a><em> article. Shout-out to <\/em><a href=\"https:\/\/www.artstation.com\/butjok\" rel=\"noopener noreferrer nofollow\"><em>Viktor Fedotov<\/em><\/a><em> for helping with translation.<\/em><\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2a5\/b98\/139\/2a5b981391e89f4fc4e28559accc98df.png\" width=\"780\" height=\"440\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/2a5\/b98\/139\/2a5b981391e89f4fc4e28559accc98df.png\"\/><figcaption><\/figcaption><\/figure>\n<p>When creating a .NET library with a partially platform-specific API, we should think about how to pack it into a NuGet package so that the installed library will work in various scenarios (.NET Framework, .NET Core, self-contained application and so on). Unfortunately, it is difficult to find a step-by-step instruction on the web that describes how to perform this task. This article is intended to be such an instruction.<\/p>\n<p>I\u2019m developing a .NET library <a href=\"https:\/\/github.com\/melanchall\/drywetmidi\" rel=\"noopener noreferrer nofollow\"><u>DryWetMIDI<\/u><\/a> which is used to work with MIDI files and MIDI devices. Most of the library API is cross-platform (within the operating systems supported by .NET of course), however the exact handling of MIDI devices is different for every operating system. Currently the corresponding API works on Windows only, but there is a strong desire to support other systems too. I\u2019m not rushing to support all the existing platforms from the start, so I\u2019m going to make the API to work on macOS first, since it\u2019s no less popular than Windows, but probably is the most popular one among music professionals.<\/p>\n<p>Of course, it doesn&#8217;t make sense to implement the whole API right away and It\u2019s much easier to make sure that everything works on a small example. This is exactly what I did, and I am suggesting you to go through the whole path to the final solution along with me. The brief list of steps is at the end of the article.<\/p>\n<h2>First attempts<\/h2>\n<p>As it\u2019s usually done in the .NET world the library is distributed through the NuGet package manager. That&#8217;s why I immediately knew that the package must contain native binaries to provide API for specific operating systems.<\/p>\n<p>In C# we can write a declaration of an external function as follows:<\/p>\n<pre><code class=\"cs\">[DllImport(\"test\")] public static extern int Foo();<\/code><\/pre>\n<p>As you can see we don\u2019t need to specify the extension of a native library since .NET will use a suitable one based on the current operating system. In other words, if <em>test.dll<\/em> and <em>test.dylib<\/em> files are located alongside our application, the function <code>Foo<\/code> will be called from test.dll on Windows, and from test.dylib on macOS. We can scale the solution to *nix systems by providing a <em>test.so<\/em> file.<\/p>\n<p>To move forward let\u2019s create a project for our test library. DryWetMIDI specifies <em>netstandard2.0<\/em> and <em>net45<\/em> <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/frameworks\" rel=\"noopener noreferrer nofollow\"><u>TFM<\/u><\/a>s within its .csproj file, so in order to be as close as possible to the real conditions I specified those target platforms for the test project as well. The project will be called <em>DualLibClassLibrary<\/em> and will contain a single file <em>Class.cs<\/em>:<\/p>\n<pre><code class=\"cs\">using System.Runtime.InteropServices;  namespace DualLibClassLibrary {     public static class Class     {         [DllImport(\"test\")]         public static extern int Foo();          public static int Bar()         {             return Foo() * 1000;         }     } }<\/code><\/pre>\n<p>Of course we also need native binaries: test.dll and test.dylib. I compiled them from a simple C code (I\u2019m going to use the same approach for the real library):<\/p>\n<p><em>for Windows<\/em><\/p>\n<pre><code class=\"cs\">int Foo() { return 123; }<\/code><\/pre>\n<p><em>for macOS<\/em><\/p>\n<pre><code class=\"cs\">int Foo() { return 456; }<\/code><\/pre>\n<p>If you\u2019re interested, test.dll and test.dylib files were created within an Azure DevOps test pipeline (in fact two pipelines: one for Windows and one for macOS). After all of that, I\u2019ll need to do all the required actions within CI builds, so right away I decided to see how it should be done for the DryWetMIDI. The pipeline is pretty simple and it consist of 3 steps:<\/p>\n<p><em>1. generate a file containing C code (PowerShell task)<\/em><\/p>\n<pre><code class=\"powershell\">New-Item \"test.c\" -ItemType File -Value \"int Foo() { return 123; }\"<\/code><\/pre>\n<p>(<code>return 456;<\/code> for macOS);<\/p>\n<p><em>2. build a library (Command Line task)<\/em><\/p>\n<pre><code class=\"bash\">gcc -v -c test.c gcc -v -shared -o test.dll test.o<\/code><\/pre>\n<p>(test.dylib for macOS);<\/p>\n<p><em>3. publish an artifact with the library (Publish Pipeline Artifact task)<\/em><\/p>\n<p>Here we have test.dll and test.dylib files implementing the same function <code>Foo<\/code>, which returns <code>123<\/code> for Windows and <code>456<\/code> for macOS, this way we can always easily check if a result of a function call is correct. We\u2019ll place the files near <em>DualLibClassLibrary.csproj<\/em>.<\/p>\n<p>Now we need to figure out how to add the files to a NuGet package so they are copied to the output directory after each application build in order to guarantee that our test library will work as it should. Since the library is cross-platform and uses the new .csproj format (SDK style), it would be great to declare packing instructions there. After doing some research I came up with this .csproj:<\/p>\n<pre><code class=\"xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\">    &lt;PropertyGroup>     &lt;TargetFrameworks>netstandard2.0;net45&lt;\/TargetFrameworks>     &lt;LangVersion>6&lt;\/LangVersion>     &lt;Configurations>Debug;Release&lt;\/Configurations>   &lt;\/PropertyGroup>    &lt;PropertyGroup>     &lt;PackageId>DualLibClassLibrary&lt;\/PackageId>     &lt;Version>1.0.0&lt;\/Version>     &lt;Authors>melanchall&lt;\/Authors>     &lt;Owners>melanchall&lt;\/Owners>     &lt;Description>Dual-lib class library&lt;\/Description>     &lt;Copyright>Copyright \u200b Melanchall 2021&lt;\/Copyright>     &lt;AutoGenerateBindingRedirects>true&lt;\/AutoGenerateBindingRedirects>   &lt;\/PropertyGroup>    &lt;ItemGroup>     &lt;Content Include=\"test.dll\">       &lt;Pack>true&lt;\/Pack>       &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>     &lt;\/Content>     &lt;Content Include=\"test.dylib\">       &lt;Pack>true&lt;\/Pack>       &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>     &lt;\/Content>   &lt;\/ItemGroup>  &lt;\/Project><\/code><\/pre>\n<p>Building the package:<\/p>\n<p><code>dotnet pack .\\DualLibClassLibrary.sln -c Release<\/code><\/p>\n<p>To check the package installation let\u2019s create a folder somewhere, add it as a packages feed in Visual Studio and put the generated file <em>DualLibClassLibrary.1.0.0.nupkg<\/em> to this folder. We are going to use classic .NET Framework on Windows to check the package installation. Now let\u2019s create a console application and install our library into it. After doing so we can see the two files have appeared in the project:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/638\/76b\/fad\/63876bfade99718a6f422dd33daa6698.png\" alt=\"test.dll and test.dylib are fetched from the package\" title=\"test.dll and test.dylib are fetched from the package\" width=\"150\" height=\"131\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/638\/76b\/fad\/63876bfade99718a6f422dd33daa6698.png\"\/><figcaption>test.dll and test.dylib are fetched from the package<\/figcaption><\/figure>\n<p>So good so far, let\u2019s write a simple code within the <em>Program.cs<\/em>:<\/p>\n<pre><code class=\"cs\">static void Main(string[] args) {     var result = DualLibClassLibrary.Class.Bar();     Console.WriteLine($\"Result = {result}. Press any key to exit...\");     Console.ReadKey(); }<\/code><\/pre>\n<p>After running the application we see the unsatisfying picture:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c14\/c69\/1b8\/c14c691b85ef458ad59fd64b2d6c8275.png\" alt=\"The program was not able to find the test.dll\" title=\"The program was not able to find the test.dll\" width=\"763\" height=\"193\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c14\/c69\/1b8\/c14c691b85ef458ad59fd64b2d6c8275.png\"\/><figcaption>The program was not able to find the test.dll<\/figcaption><\/figure>\n<p>Well&#8230; let\u2019s take a look into <em>bin\/Debug<\/em> folder:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/00b\/a44\/6a5\/00ba446a5cf55e58496d8105b227be2d.png\" alt=\"test.dll and test.dylib files are missing in the application\u2019s output directory\" title=\"test.dll and test.dylib files are missing in the application\u2019s output directory\" width=\"149\" height=\"91\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/00b\/a44\/6a5\/00ba446a5cf55e58496d8105b227be2d.png\"\/><figcaption>test.dll and test.dylib files are missing in the application\u2019s output directory<\/figcaption><\/figure>\n<p>Surprisingly enough the files are missing. It is indeed strange since we\u2019ve specified the <code>&lt;CopyToOutputDirectory><\/code> item for them and we do actually see the files within the project structure. But everything becomes clear after we take a look into the .csproj file:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/118\/683\/7c5\/1186837c5f17b44722a3434615f7c9f4.png\" alt=\"Chaos in the .csproj\" title=\"Chaos in the .csproj\" width=\"264\" height=\"130\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/118\/683\/7c5\/1186837c5f17b44722a3434615f7c9f4.png\"\/><figcaption>Chaos in the .csproj<\/figcaption><\/figure>\n<p>We have two weird things here. Firstly the <code>&lt;CopyToOutputDirectory><\/code> item is missing. And secondly, test.dylib has been added as <code>&lt;None><\/code> item but test.dll has been added as <code>&lt;Content><\/code> item. If we take a look into the .nupkg file with <a href=\"https:\/\/github.com\/NuGetPackageExplorer\/NuGetPackageExplorer\" rel=\"noopener noreferrer nofollow\"><u>NuGet Package Explorer<\/u><\/a>, we\u2019ll see the following manifest:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?> &lt;package xmlns=\"http:\/\/schemas.microsoft.com\/packaging\/2012\/06\/nuspec.xsd\">   &lt;metadata>     &lt;id>DualLibClassLibrary&lt;\/id>     &lt;version>1.0.0&lt;\/version>     &lt;authors>melanchall&lt;\/authors>     &lt;owners>&lt;\/owners>     &lt;requireLicenseAcceptance>false&lt;\/requireLicenseAcceptance>     &lt;description>Dual-lib class library&lt;\/description>     &lt;copyright>Copyright \u200b Melanchall 2021&lt;\/copyright>     &lt;dependencies>       &lt;group targetFramework=\".NETFramework4.5\" \/>       &lt;group targetFramework=\".NETStandard2.0\" \/>     &lt;\/dependencies>     &lt;contentFiles>       &lt;files include=\"any\/net45\/test.dll\" buildAction=\"Content\" \/>       &lt;files include=\"any\/netstandard2.0\/test.dll\" buildAction=\"Content\" \/>       &lt;files include=\"any\/net45\/test.dylib\" buildAction=\"Content\" \/>       &lt;files include=\"any\/netstandard2.0\/test.dylib\" buildAction=\"Content\" \/>     &lt;\/contentFiles>   &lt;\/metadata> &lt;\/package><\/code><\/pre>\n<p>As you can see, sadly, the files were added without the <code>copyToOutput<\/code> attribute (you can read about the attribute here in the table: <a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/reference\/nuspec#using-the-contentfiles-element-for-content-files\" rel=\"noopener noreferrer nofollow\"><u>Using the contentFiles element for content files<\/u><\/a>).<\/p>\n<h2>Copying files to the application\u2019s output directory<\/h2>\n<p>After looking through the web (including issues on GitHub, answers on StackOverflow and official Microsoft documentation), I modified the files packing elements in the library\u2019s .csproj:<\/p>\n<pre><code class=\"xml\">&lt;Content Include=\"test.dll\">   &lt;Pack>true&lt;\/Pack>   &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>   &lt;PackageCopyToOutput>true&lt;\/PackageCopyToOutput>   &lt;PackagePath>contentFiles;content&lt;\/PackagePath> &lt;\/Content> &lt;Content Include=\"test.dylib\">   &lt;Pack>true&lt;\/Pack>   &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>   &lt;PackageCopyToOutput>true&lt;\/PackageCopyToOutput>   &lt;PackagePath>contentFiles;content&lt;\/PackagePath> &lt;\/Content><\/code><\/pre>\n<p>The <code>&lt;PackageCopyToOutput><\/code> item is exactly what we need to make the <code>copyToOutput<\/code> attribute to appear in the package manifest. Also we explicitly specified destination folders for the files to avoid directories like <em>any<\/em>. If you\u2019re interested in this topic you can read more here: <a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/reference\/msbuild-targets#including-content-in-a-package\" rel=\"noopener noreferrer nofollow\"><u>Including content in a package<\/u><\/a>.<\/p>\n<p>Let\u2019s build the project one more time and check the manifest:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?> &lt;package xmlns=\"http:\/\/schemas.microsoft.com\/packaging\/2012\/06\/nuspec.xsd\">   &lt;metadata>     &lt;id>DualLibClassLibrary&lt;\/id>     &lt;version>1.0.1&lt;\/version>     &lt;authors>melanchall&lt;\/authors>     &lt;owners>&lt;\/owners>     &lt;requireLicenseAcceptance>false&lt;\/requireLicenseAcceptance>     &lt;description>Dual-lib class library&lt;\/description>     &lt;copyright>Copyright \u200b Melanchall 2021&lt;\/copyright>     &lt;dependencies>       &lt;group targetFramework=\".NETFramework4.5\" \/>       &lt;group targetFramework=\".NETStandard2.0\" \/>     &lt;\/dependencies>     &lt;contentFiles>       &lt;files include=\"test.dll\" buildAction=\"Content\" copyToOutput=\"true\" \/>       &lt;files include=\"test.dylib\" buildAction=\"Content\" copyToOutput=\"true\" \/>     &lt;\/contentFiles>   &lt;\/metadata> &lt;\/package><\/code><\/pre>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d8f\/efa\/24c\/d8fefa24cbb80de41ef3fb20ab259c3c.png\" alt=\"copyToOutput didn\u2019t help\" title=\"copyToOutput didn\u2019t help\" width=\"763\" height=\"193\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d8f\/efa\/24c\/d8fefa24cbb80de41ef3fb20ab259c3c.png\"\/><figcaption>copyToOutput didn\u2019t help<\/figcaption><\/figure>\n<p>Another failure. If we are going to try the same thing in .NET5, we\u2019ll get:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/42c\/f02\/1bc\/42cf021bcabe60a67e06dbf53ca71294.png\" alt=\"Still no files in the application\u2019s output directory\" title=\"Still no files in the application\u2019s output directory\" width=\"759\" height=\"193\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/42c\/f02\/1bc\/42cf021bcabe60a67e06dbf53ca71294.png\"\/><figcaption>Still no files in the application\u2019s output directory<\/figcaption><\/figure>\n<p>Basically there\u2019s no difference except a slightly changed message of the exception. After submitting a GitHub issue I received the answer:<\/p>\n<blockquote>\n<p><em>Please see our <\/em><a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/reference\/nuspec#package-folder-structure\" rel=\"noopener noreferrer nofollow\"><em><u>docs on<\/u><\/em><\/a><em> <\/em><code>contentFiles<\/code><em>. It supports adding different content depending on project&#8217;s target framework and language, and therefore needs files in a specific structure which your package is not currently using.<\/em><\/p>\n<\/blockquote>\n<p>It turns out that I overlooked a few details in the documentation: if you add files into directories named like <em>contentFiles\/any\/netstandard2.0<\/em> instead of simply <em>contentFiles<\/em>, a .props file is created automatically and it contains proper elements for copying files to the application\u2019s output directory. However before I received any answer I did my own research and came up with a different approach. And in hindsight it was the right decision and the reason is that you will not have all of the files installed properly from a package into your .NET Framework application when using the <code>contentFiles<\/code> folder. I strongly believe it is an important scenario and thus it must be supported.<\/p>\n<p>There is an article on the Microsoft documentation with a promising title: <a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/guides\/native-packages\" rel=\"noopener noreferrer nofollow\"><u>Creating native packages<\/u><\/a>. The article itself is not very informative, but we can learn something useful from it \u2013 the fact that we can create a .targets file with <code>&lt;CopyToOutputDirectory><\/code> elements specified for our files. The .targets file must be included in a package along with native libraries. Well, let\u2019s create <em>DualLibClassLibrary.targets<\/em> file:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?> &lt;Project ToolsVersion=\"4.0\" xmlns=\"http:\/\/schemas.microsoft.com\/developer\/msbuild\/2003\">   &lt;ItemGroup>     &lt;None Include=\"$(MSBuildThisFileDirectory)test.dll\">       &lt;Link>test.dll&lt;\/Link>       &lt;CopyToOutputDirectory>PreserveNewest&lt;\/CopyToOutputDirectory>     &lt;\/None>     &lt;None Include=\"$(MSBuildThisFileDirectory)test.dylib\">       &lt;Link>test.dylib&lt;\/Link>       &lt;CopyToOutputDirectory>PreserveNewest&lt;\/CopyToOutputDirectory>     &lt;\/None>   &lt;\/ItemGroup> &lt;\/Project><\/code><\/pre>\n<p>And in the <em>DualLibClassLibrary.csproj<\/em> file we write:<\/p>\n<pre><code class=\"xml\">&lt;ItemGroup>   &lt;None Include=\"test.dll\">     &lt;CopyToOutputDirectory>PreserveNewest&lt;\/CopyToOutputDirectory>     &lt;PackagePath>build\\&lt;\/PackagePath>     &lt;Pack>true&lt;\/Pack>   &lt;\/None>   &lt;None Include=\"test.dylib\">     &lt;CopyToOutputDirectory>PreserveNewest&lt;\/CopyToOutputDirectory>     &lt;PackagePath>build\\&lt;\/PackagePath>     &lt;Pack>true&lt;\/Pack>   &lt;\/None>   &lt;None Include=\"DualLibClassLibrary.targets\">     &lt;PackagePath>build\\&lt;\/PackagePath>     &lt;Pack>true&lt;\/Pack>   &lt;\/None> &lt;\/ItemGroup><\/code><\/pre>\n<p>By building a package for the version 1.0.2, installing it to our .NET Framework console application and running it we have:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/031\/6b7\/f49\/0316b7f49240fd8877e41b0901d27d65.png\" alt=\"We have a different error now\" title=\"We have a different error now\" width=\"763\" height=\"195\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/031\/6b7\/f49\/0316b7f49240fd8877e41b0901d27d65.png\"\/><figcaption>We have a different error now<\/figcaption><\/figure>\n<p>This exception is usually thrown when the application&#8217;s process is 64-bit while the native library is 32-bit or the other way around. But in this case I built the libraries on 64-bit systems and the application was running on a 64-bit operating system too. Well, it seems our journey continues.<\/p>\n<h2>Support for 32-bit and 64-bit processes<\/h2>\n<p>If we open the properties of our console application project in Visual Studio inside the Build tab, we can see this checkbox:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/730\/e14\/3ae\/730e143aebd14ccc26a2c7b49ce26b85.png\" alt=\"A process will be 32-bit\" title=\"A process will be 32-bit\" width=\"342\" height=\"54\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/730\/e14\/3ae\/730e143aebd14ccc26a2c7b49ce26b85.png\"\/><figcaption>A process will be 32-bit<\/figcaption><\/figure>\n<p>It turns out, this option is checked by default for a .NET Framework project and the application\u2019s process will be 32-bit even on a 64-bit operating system. It\u2019s funny that for .NET Core\/.NET 5 projects this option is turned off by default:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d1a\/862\/8da\/d1a8628da64865510a26c02cd24c5fdb.png\" alt=\"Checkbox is unchecked for .NET Core\" title=\"Checkbox is unchecked for .NET Core\" width=\"344\" height=\"84\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d1a\/862\/8da\/d1a8628da64865510a26c02cd24c5fdb.png\"\/><figcaption>Checkbox is unchecked for .NET Core<\/figcaption><\/figure>\n<p>Of course, we can turn this option off and the application will finally print the correct result:<\/p>\n<p><code>Result = 123000. Press any key to exit...<\/code><\/p>\n<p>But that\u2019s obviously not the solution for the following reasons:<\/p>\n<ol>\n<li>\n<p>we won\u2019t be able to use the library in 32-bit processes;<\/p>\n<\/li>\n<li>\n<p>it requires additional actions from a user (unchecking a checkbox);<\/p>\n<\/li>\n<li>\n<p>the classic default scenario (create new .NET Framework application without any additional actions) won\u2019t be supported.<\/p>\n<\/li>\n<\/ol>\n<p>Certainly this is not an option and the problem must be solved. In fact the solution is obvious: let\u2019s just create native binaries for each operating system in two versions \u2013 32-bit and 64-bit. It means the package will be larger in size since it will contain 4 platform-specific libraries instead of 2. I don\u2019t see anything wrong with this approach because the files are small, and thus I am going to proceed with it (and I can\u2019t see how it can be done in another way).<\/p>\n<p>Let me briefly tell you about how I build 32-bit libraries. As I\u2019ve mentioned above I create binaries within Azure DevOps pipelines using gcc. gcc is called with <code>-m32<\/code> flag which in theory tells gcc to build a 32-bit library. This is fine on build agents with macOS images, but on Windows I got quite a few nasty log messages:<\/p>\n<p><code>C:\/ProgramData\/Chocolatey\/lib\/mingw\/tools\/install\/mingw64\/bin\/..\/lib\/gcc\/x86_64-w64-mingw32\/8.1.0\/..\/..\/..\/..\/x86_64-w64-mingw32\/bin\/ld.exe: skipping incompatible C:\/ProgramData\/Chocolatey\/lib\/mingw\/tools\/install\/mingw64\/bin\/..\/lib\/gcc\/x86_64-w64-mingw32\/8.1.0\/..\/..\/..\/..\/x86_64-w64-mingw32\/lib\\libuser32.a when searching for -luser32<\/code><\/p>\n<p><code>...<\/code><\/p>\n<p><code>C:\/ProgramData\/Chocolatey\/lib\/mingw\/tools\/install\/mingw64\/bin\/..\/lib\/gcc\/x86_64-w64-mingw32\/8.1.0\/..\/..\/..\/..\/x86_64-w64-mingw32\/lib\/libmsvcrt.a when searching for -lmsvcrt C:\/ProgramData\/Chocolatey\/lib\/mingw\/tools\/install\/mingw64\/bin\/..\/lib\/gcc\/x86_64-w64-mingw32\/8.1.0\/..\/..\/..\/..\/x86_64-w64-mingw32\/bin\/ld.exe: cannot find -lmsvcrt collect2.exe: error: ld returned 1 exit status<\/code><\/p>\n<p>I asked a question about these errors <a href=\"https:\/\/stackoverflow.com\/questions\/67671973\/gcc-m32-not-working-on-windows-hosted-agents\" rel=\"noopener noreferrer nofollow\"><u>on StackOverflow<\/u><\/a> and <a href=\"https:\/\/developercommunity.visualstudio.com\/t\/gcc-m32-not-working-on-windows-hosted-agents\/1430375\" rel=\"noopener noreferrer nofollow\"><u>on Microsoft Developer Community<\/u><\/a> site, and got answers stating that the 32-bit version of the MinGW is not preinstalled on Microsoft-hosted agents, which causes the error. After trying many options, I chose the <a href=\"https:\/\/github.com\/brechtsanders\/winlibs_mingw\" rel=\"noopener noreferrer nofollow\"><u>brechtsanders\/winlibs_mingw<\/u><\/a> project creating a simple PowerShell script:<\/p>\n<pre><code class=\"powershell\">Write-Host \"Downloading winlibs...\" Invoke-WebRequest -Uri \"https:\/\/github.com\/brechtsanders\/winlibs_mingw\/releases\/download\/11.1.0-12.0.0-9.0.0-r1\/winlibs-i686-posix-dwarf-gcc-11.1.0-mingw-w64-9.0.0-r1.zip\" -OutFile \"winlibs.zip\" Write-Host \"Downloaded.\"  Write-Host \"Extracting winlibs...\" Expand-Archive -LiteralPath 'winlibs.zip' -DestinationPath \"winlibs\" Write-Host \"Extracted.\"  Write-Host \"Building DLL...\" $gccPath = Get-ChildItem -Path \"winlibs\" -File -Filter \"i686-w64-mingw32-gcc.exe\" -Recurse  &amp; $gccPath.FullName -c test.c -m32 &amp; $gccPath.FullName -shared -o test.dll test.o -m32 Write-Host \"Built.\"<\/code><\/pre>\n<p>By using the <em>i686-w64-mingw32-gcc.exe<\/em> compiler from the archive, I was finally able to build the 32-bit test.dll. Hooray!<\/p>\n<p>Now we need to figure out how to tell our library which API to use: 32-bit or 64-bit one. I am quite sure there are multiple different approaches, but I settled on the following one:<\/p>\n<ol>\n<li>\n<p>build test32.dll, test64.dll, test32.dylib and test64.dylib native binaries;<\/p>\n<\/li>\n<li>\n<p>create abstract class <code>Api<\/code> containing abstract methods that correspond to our managed API for internal usage;<\/p>\n<\/li>\n<li>\n<p>create two subclasses of <code>Api<\/code> \u2013 <code>Api32<\/code> and <code>Api64<\/code> \u2013 which will implement abstract API from the base class calling unmanaged API from test32 and test64 correspondingly;<\/p>\n<\/li>\n<li>\n<p>create class <code>ApiProvider<\/code> with the <code>Api<\/code> property which will return an implementation based on a current process bitness.<\/p>\n<\/li>\n<\/ol>\n<p><strong>UPD:<\/strong> Starting with .NET Core 3.0 you can use <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.runtime.interopservices.nativelibrary\" rel=\"noopener noreferrer nofollow\">NativeLibrary<\/a> class and its <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.runtime.interopservices.nativelibrary.setdllimportresolver?#system-runtime-interopservices-nativelibrary-setdllimportresolver(system-reflection-assembly-system-runtime-interopservices-dllimportresolver)\" rel=\"noopener noreferrer nofollow\">SetDllImportResolver<\/a> method. The method allows to simplify the process below and get rid of duplicated signatures of external functions (you will be able to choose required native binary name in runtime by the current process bitness).<\/p>\n<p>Here the code of the classes:<\/p>\n<p><strong>Api.cs<\/strong><\/p>\n<pre><code class=\"cs\">namespace DualLibClassLibrary {     internal abstract class Api     {         public abstract int Method();     } }<\/code><\/pre>\n<p><strong>Api32.cs<\/strong><\/p>\n<pre><code class=\"cs\">using System.Runtime.InteropServices;  namespace DualLibClassLibrary {     internal sealed class Api32 : Api     {         [DllImport(\"test32\")]         public static extern int Foo();          public override int Method()         {             return Foo();         }     } }<\/code><\/pre>\n<p><strong>Api64.cs<\/strong><\/p>\n<pre><code class=\"cs\">using System.Runtime.InteropServices;  namespace DualLibClassLibrary {     internal sealed class Api64 : Api     {         [DllImport(\"test64\")]         public static extern int Foo();          public override int Method()         {             return Foo();         }     } }<\/code><\/pre>\n<p><strong>ApiProvider.cs<\/strong><\/p>\n<pre><code class=\"cs\">using System;  namespace DualLibClassLibrary {     internal static class ApiProvider     {         private static readonly bool Is64Bit = IntPtr.Size == 8;         private static Api _api;          public static Api Api         {             get             {                 if (_api == null)                     _api = Is64Bit ? (Api)new Api64() : new Api32();                  return _api;             }         }     } }<\/code><\/pre>\n<p>And then the code of <code>Class<\/code> class will be:<\/p>\n<pre><code class=\"cs\">namespace DualLibClassLibrary {     public static class Class     {         public static int Bar()         {             return ApiProvider.Api.Method() * 1000;         }     } } <\/code><\/pre>\n<p>After a new version of the package is created (of course with new files added to the <em>DualLibClassLibrary.targets<\/em> and <em>DualLibClassLibrary.csproj<\/em> before that) we\u2019ll see that the method of our library works correctly no matter whether application\u2019s process is 32-bit or 64-bit.<\/p>\n<h2>Conclusion<\/h2>\n<p>Here you have a complete chronology of my trials on the creation of a NuGet package with platform-specific API, and it would be useful to briefly list the main points (I promised the instructions after all):<\/p>\n<ol>\n<li>\n<p>create native binaries in 32-bit and 64-bit versions;<\/p>\n<\/li>\n<li>\n<p>place them near a library project (you can place them in any folder and specify the folder\u2019s path in .csproj and .targets files);<\/p>\n<\/li>\n<li>\n<p>add .targets file with the <code>&lt;CopyToOutputDirectory><\/code> element (with desired value) added for each native file;<\/p>\n<\/li>\n<li>\n<p>add elements for packing .targets file (should be placed in the build folder of a package) and all native binaries in the .csproj file;<\/p>\n<\/li>\n<li>\n<p>implement a way to select a native file based on the fact whether a process is 32-bit or 64-bit.<\/p>\n<\/li>\n<\/ol>\n<p>Simple as that! The solution of our test library is available here: <a href=\"https:\/\/www.dropbox.com\/s\/9u9h3vb0ke9l7ef\/DualLibClassLibrary.zip?dl=0\" rel=\"noopener noreferrer nofollow\"><u>DualLibClassLibrary.zip<\/u><\/a>. The library was tested in the following scenarios on Windows and macOS:<\/p>\n<ol>\n<li>\n<p>.NET Framework application;<\/p>\n<\/li>\n<li>\n<p>.NET Core \/ .NET 5 application;<\/p>\n<\/li>\n<li>\n<p>Self-contained application.<\/p>\n<\/li>\n<\/ol>\n<p>As for support of 32-bit and 64-bit processes \u2013 I\u2019ve checked only on Windows, I&#8217;m not sure how to check it on macOS. <strong>UPD:<\/strong> I have only 64-bit dylib file in the package.<\/p>\n<p>It is worth noting that <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/5.0\/5.0-supported-os.md\" rel=\"noopener noreferrer nofollow\"><u>.NET currently supports only desktop operating systems<\/u><\/a>. However, <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/6.0\/supported-os.md\" rel=\"noopener noreferrer nofollow\"><u>support for mobile platforms is planned in .NET 6<\/u><\/a>. I&#8217;m not sure if the approach described above will work in that case too. I suppose a dylib file will work on iOS (or not?), but we definitely need to take care of Android support separately. Maybe someone has already done it and can share instructions in the comments? <strong>UPD:<\/strong> As .NET 6 and then .NET MAUI have been released you can apply the process in the article to mobile platforms too. Also for iOS you need to provide an <strong>a<\/strong> file instead of dylib one, please see details in <a href=\"https:\/\/github.com\/melanchall\/drywetmidi\/discussions\/235\" rel=\"noopener noreferrer nofollow\">this discussion<\/a>.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/565908\/\"> https:\/\/habr.com\/ru\/articles\/565908\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><em>It&#8217;s the English version of <\/em><a href=\"https:\/\/habr.com\/ru\/post\/564476\" rel=\"noopener noreferrer nofollow\"><em>this<\/em><\/a><em> article. Shout-out to <\/em><a href=\"https:\/\/www.artstation.com\/butjok\" rel=\"noopener noreferrer nofollow\"><em>Viktor Fedotov<\/em><\/a><em> for helping with translation.<\/em><\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>When creating a .NET library with a partially platform-specific API, we should think about how to pack it into a NuGet package so that the installed library will work in various scenarios (.NET Framework, .NET Core, self-contained application and so on). Unfortunately, it is difficult to find a step-by-step instruction on the web that describes how to perform this task. This article is intended to be such an instruction.<\/p>\n<p>I\u2019m developing a .NET library <a href=\"https:\/\/github.com\/melanchall\/drywetmidi\" rel=\"noopener noreferrer nofollow\"><u>DryWetMIDI<\/u><\/a> which is used to work with MIDI files and MIDI devices. Most of the library API is cross-platform (within the operating systems supported by .NET of course), however the exact handling of MIDI devices is different for every operating system. Currently the corresponding API works on Windows only, but there is a strong desire to support other systems too. I\u2019m not rushing to support all the existing platforms from the start, so I\u2019m going to make the API to work on macOS first, since it\u2019s no less popular than Windows, but probably is the most popular one among music professionals.<\/p>\n<p>Of course, it doesn&#8217;t make sense to implement the whole API right away and It\u2019s much easier to make sure that everything works on a small example. This is exactly what I did, and I am suggesting you to go through the whole path to the final solution along with me. The brief list of steps is at the end of the article.<\/p>\n<h2>First attempts<\/h2>\n<p>As it\u2019s usually done in the .NET world the library is distributed through the NuGet package manager. That&#8217;s why I immediately knew that the package must contain native binaries to provide API for specific operating systems.<\/p>\n<p>In C# we can write a declaration of an external function as follows:<\/p>\n<pre><code class=\"cs\">[DllImport(\"test\")] public static extern int Foo();<\/code><\/pre>\n<p>As you can see we don\u2019t need to specify the extension of a native library since .NET will use a suitable one based on the current operating system. In other words, if <em>test.dll<\/em> and <em>test.dylib<\/em> files are located alongside our application, the function <code>Foo<\/code> will be called from test.dll on Windows, and from test.dylib on macOS. We can scale the solution to *nix systems by providing a <em>test.so<\/em> file.<\/p>\n<p>To move forward let\u2019s create a project for our test library. DryWetMIDI specifies <em>netstandard2.0<\/em> and <em>net45<\/em> <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/frameworks\" rel=\"noopener noreferrer nofollow\"><u>TFM<\/u><\/a>s within its .csproj file, so in order to be as close as possible to the real conditions I specified those target platforms for the test project as well. The project will be called <em>DualLibClassLibrary<\/em> and will contain a single file <em>Class.cs<\/em>:<\/p>\n<pre><code class=\"cs\">using System.Runtime.InteropServices;  namespace DualLibClassLibrary {     public static class Class     {         [DllImport(\"test\")]         public static extern int Foo();          public static int Bar()         {             return Foo() * 1000;         }     } }<\/code><\/pre>\n<p>Of course we also need native binaries: test.dll and test.dylib. I compiled them from a simple C code (I\u2019m going to use the same approach for the real library):<\/p>\n<p><em>for Windows<\/em><\/p>\n<pre><code class=\"cs\">int Foo() { return 123; }<\/code><\/pre>\n<p><em>for macOS<\/em><\/p>\n<pre><code class=\"cs\">int Foo() { return 456; }<\/code><\/pre>\n<p>If you\u2019re interested, test.dll and test.dylib files were created within an Azure DevOps test pipeline (in fact two pipelines: one for Windows and one for macOS). After all of that, I\u2019ll need to do all the required actions within CI builds, so right away I decided to see how it should be done for the DryWetMIDI. The pipeline is pretty simple and it consist of 3 steps:<\/p>\n<p><em>1. generate a file containing C code (PowerShell task)<\/em><\/p>\n<pre><code class=\"powershell\">New-Item \"test.c\" -ItemType File -Value \"int Foo() { return 123; }\"<\/code><\/pre>\n<p>(<code>return 456;<\/code> for macOS);<\/p>\n<p><em>2. build a library (Command Line task)<\/em><\/p>\n<pre><code class=\"bash\">gcc -v -c test.c gcc -v -shared -o test.dll test.o<\/code><\/pre>\n<p>(test.dylib for macOS);<\/p>\n<p><em>3. publish an artifact with the library (Publish Pipeline Artifact task)<\/em><\/p>\n<p>Here we have test.dll and test.dylib files implementing the same function <code>Foo<\/code>, which returns <code>123<\/code> for Windows and <code>456<\/code> for macOS, this way we can always easily check if a result of a function call is correct. We\u2019ll place the files near <em>DualLibClassLibrary.csproj<\/em>.<\/p>\n<p>Now we need to figure out how to add the files to a NuGet package so they are copied to the output directory after each application build in order to guarantee that our test library will work as it should. Since the library is cross-platform and uses the new .csproj format (SDK style), it would be great to declare packing instructions there. After doing some research I came up with this .csproj:<\/p>\n<pre><code class=\"xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\">    &lt;PropertyGroup>     &lt;TargetFrameworks>netstandard2.0;net45&lt;\/TargetFrameworks>     &lt;LangVersion>6&lt;\/LangVersion>     &lt;Configurations>Debug;Release&lt;\/Configurations>   &lt;\/PropertyGroup>    &lt;PropertyGroup>     &lt;PackageId>DualLibClassLibrary&lt;\/PackageId>     &lt;Version>1.0.0&lt;\/Version>     &lt;Authors>melanchall&lt;\/Authors>     &lt;Owners>melanchall&lt;\/Owners>     &lt;Description>Dual-lib class library&lt;\/Description>     &lt;Copyright>Copyright \u200b Melanchall 2021&lt;\/Copyright>     &lt;AutoGenerateBindingRedirects>true&lt;\/AutoGenerateBindingRedirects>   &lt;\/PropertyGroup>    &lt;ItemGroup>     &lt;Content Include=\"test.dll\">       &lt;Pack>true&lt;\/Pack>       &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>     &lt;\/Content>     &lt;Content Include=\"test.dylib\">       &lt;Pack>true&lt;\/Pack>       &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>     &lt;\/Content>   &lt;\/ItemGroup>  &lt;\/Project><\/code><\/pre>\n<p>Building the package:<\/p>\n<p><code>dotnet pack .\\DualLibClassLibrary.sln -c Release<\/code><\/p>\n<p>To check the package installation let\u2019s create a folder somewhere, add it as a packages feed in Visual Studio and put the generated file <em>DualLibClassLibrary.1.0.0.nupkg<\/em> to this folder. We are going to use classic .NET Framework on Windows to check the package installation. Now let\u2019s create a console application and install our library into it. After doing so we can see the two files have appeared in the project:<\/p>\n<figure class=\"\"><figcaption>test.dll and test.dylib are fetched from the package<\/figcaption><\/figure>\n<p>So good so far, let\u2019s write a simple code within the <em>Program.cs<\/em>:<\/p>\n<pre><code class=\"cs\">static void Main(string[] args) {     var result = DualLibClassLibrary.Class.Bar();     Console.WriteLine($\"Result = {result}. Press any key to exit...\");     Console.ReadKey(); }<\/code><\/pre>\n<p>After running the application we see the unsatisfying picture:<\/p>\n<figure class=\"full-width\"><figcaption>The program was not able to find the test.dll<\/figcaption><\/figure>\n<p>Well&#8230; let\u2019s take a look into <em>bin\/Debug<\/em> folder:<\/p>\n<figure class=\"\"><figcaption>test.dll and test.dylib files are missing in the application\u2019s output directory<\/figcaption><\/figure>\n<p>Surprisingly enough the files are missing. It is indeed strange since we\u2019ve specified the <code>&lt;CopyToOutputDirectory><\/code> item for them and we do actually see the files within the project structure. But everything becomes clear after we take a look into the .csproj file:<\/p>\n<figure class=\"\"><figcaption>Chaos in the .csproj<\/figcaption><\/figure>\n<p>We have two weird things here. Firstly the <code>&lt;CopyToOutputDirectory><\/code> item is missing. And secondly, test.dylib has been added as <code>&lt;None><\/code> item but test.dll has been added as <code>&lt;Content><\/code> item. If we take a look into the .nupkg file with <a href=\"https:\/\/github.com\/NuGetPackageExplorer\/NuGetPackageExplorer\" rel=\"noopener noreferrer nofollow\"><u>NuGet Package Explorer<\/u><\/a>, we\u2019ll see the following manifest:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?> &lt;package xmlns=\"http:\/\/schemas.microsoft.com\/packaging\/2012\/06\/nuspec.xsd\">   &lt;metadata>     &lt;id>DualLibClassLibrary&lt;\/id>     &lt;version>1.0.0&lt;\/version>     &lt;authors>melanchall&lt;\/authors>     &lt;owners>&lt;\/owners>     &lt;requireLicenseAcceptance>false&lt;\/requireLicenseAcceptance>     &lt;description>Dual-lib class library&lt;\/description>     &lt;copyright>Copyright \u200b Melanchall 2021&lt;\/copyright>     &lt;dependencies>       &lt;group targetFramework=\".NETFramework4.5\" \/>       &lt;group targetFramework=\".NETStandard2.0\" \/>     &lt;\/dependencies>     &lt;contentFiles>       &lt;files include=\"any\/net45\/test.dll\" buildAction=\"Content\" \/>       &lt;files include=\"any\/netstandard2.0\/test.dll\" buildAction=\"Content\" \/>       &lt;files include=\"any\/net45\/test.dylib\" buildAction=\"Content\" \/>       &lt;files include=\"any\/netstandard2.0\/test.dylib\" buildAction=\"Content\" \/>     &lt;\/contentFiles>   &lt;\/metadata> &lt;\/package><\/code><\/pre>\n<p>As you can see, sadly, the files were added without the <code>copyToOutput<\/code> attribute (you can read about the attribute here in the table: <a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/reference\/nuspec#using-the-contentfiles-element-for-content-files\" rel=\"noopener noreferrer nofollow\"><u>Using the contentFiles element for content files<\/u><\/a>).<\/p>\n<h2>Copying files to the application\u2019s output directory<\/h2>\n<p>After looking through the web (including issues on GitHub, answers on StackOverflow and official Microsoft documentation), I modified the files packing elements in the library\u2019s .csproj:<\/p>\n<pre><code class=\"xml\">&lt;Content Include=\"test.dll\">   &lt;Pack>true&lt;\/Pack>   &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>   &lt;PackageCopyToOutput>true&lt;\/PackageCopyToOutput>   &lt;PackagePath>contentFiles;content&lt;\/PackagePath> &lt;\/Content> &lt;Content Include=\"test.dylib\">   &lt;Pack>true&lt;\/Pack>   &lt;CopyToOutputDirectory>Always&lt;\/CopyToOutputDirectory>   &lt;PackageCopyToOutput>true&lt;\/PackageCopyToOutput>   &lt;PackagePath>contentFiles;content&lt;\/PackagePath> &lt;\/Content><\/code><\/pre>\n<p>The <code>&lt;PackageCopyToOutput><\/code> item is exactly what we need to make the <code>copyToOutput<\/code> attribute to appear in the package manifest. Also we explicitly specified destination folders for the files to avoid directories like <em>any<\/em>. If you\u2019re interested in this topic you can read more here: <a href=\"https:\/\/docs.microsoft.com\/en-us\/nuget\/reference\/msbuild-targets#including-content-in-a-package\" rel=\"noopener noreferrer nofollow\"><u>Including content in a package<\/u><\/a>.<\/p>\n<p>Let\u2019s build the project one more time and check the manifest:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?> &lt;package xmlns=\"http:\/\/schemas.microsoft.com\/packaging\/2012\/06\/nuspec.xsd\">   &lt;metadata>     &lt;id>DualLibClassLibrary&lt;\/id>     &lt;version>1.0.1&lt;\/version>     &lt;authors>melanchall&lt;\/authors>     &lt;owners>&lt;\/owners>     &lt;requireLicenseAcceptance>false&lt;\/requireLicenseAcceptance>     &lt;description>Dual-lib class library&lt;\/description>     &lt;copyright>Copyright \u200b Melanchall 2021&lt;\/copyright>     &lt;dependencies>       &lt;group targetFramework=\".NETFramework4.5\" \/>       &lt;group targetFramework=\".NETStandard2.0\" \/>     &lt;\/dependencies>     &lt;contentFiles>       &lt;files include=\"test.dll\" buildAction=\"Content\" copyToOutput=\"true\" \/>       &lt;files include=\"test.dylib\" buildAction=\"Content\" copyToOutput=\"true\" \/>     &lt;\/contentFiles>   &lt;\/metadata> &lt;\/package><\/code><\/pre>\n<figure class=\"full-width\"><figcaption>copyToOutput didn\u2019t help<\/figcaption><\/figure>\n<p>Another failure. If we are going to try the same thing in .NET5, we\u2019ll get:<\/p>\n<figure class=\"full-width\"><figcaption>Still no files in the application\u2019s output directory<\/figcaption><\/figure>\n<p>Basically there\u2019s no difference<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-385557","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/385557","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=385557"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/385557\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=385557"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=385557"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=385557"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}