SkiaSharp icon indicating copy to clipboard operation
SkiaSharp copied to clipboard

[BUG] VS build error when referencing SkiaSharp from an SDK style csproj targetting .NET framework 4.8

Open heathdavies-eaton opened this issue 2 years ago • 4 comments

Description

Visual Studio build error when referencing SkiaSharp from an SDK style csproj targetting .NET framework 4.8

Code

My .csproj file is as follows.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>
	<ItemGroup>
    <PackageReference Include="SkiaSharp" Version="2.88.3" />
  </ItemGroup>
</Project>

I then try and compile it using VS 2022 and receive a build error.

Expected Behavior

The project builds with no errors.

Actual Behavior

I receive the following build error:

Error	NETSDK1022	Duplicate 'Content' items were included. The .NET SDK includes 'Content' items from your project directory by default. You can either remove these items from your project file, or set the 'EnableDefaultContentItems' property to 'false' if you want to explicitly include them in your project file. For more information, see https://aka.ms/sdkimplicititems. The duplicate items were: 'C:\Users\XXX\.nuget\packages\skiasharp.nativeassets.win32\2.88.3\buildTransitive\net462\..\..\runtimes\win-x86\native\libSkiaSharp.dll'; 'C:\Users\XXX\.nuget\packages\skiasharp.nativeassets.win32\2.88.3\buildTransitive\net462\..\..\runtimes\win-x64\native\libSkiaSharp.dll'; 'C:\Users\XXX\.nuget\packages\skiasharp.nativeassets.win32\2.88.3\buildTransitive\net462\..\..\runtimes\win-arm64\native\libSkiaSharp.dll'	ConsoleApp3	C:\Program Files\dotnet\sdk\7.0.203\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.DefaultItems.Shared.targets

Basic Information

  • Version with issue: 2.88.3
  • Last known good version: N/A
  • IDE: Visual Studio 2022
  • Platform Target Frameworks:
    • Windows Classic: Windows Enterprise 10

Other information If I target net7.0 the project build ok. Also using a traditional non-SDK style project targeting .NET framework 4.8 this also compiles.

heathdavies-eaton avatar Apr 27 '23 12:04 heathdavies-eaton

Hi, @heathdavies-eaton , Did you solve this issue in the end?

ArlenLi avatar Aug 12 '23 11:08 ArlenLi

Hello @ArlenLi , no I didn't find a solution. In the end I just had to use a non-SDK style project.

heathdavies-eaton avatar Aug 16 '23 07:08 heathdavies-eaton

@ArlenLi @heathdavies-eaton,

we are suffering the same problem, maybe a workaround would be excluding the macOS Nuget package native assets, because net48 is not platform independent. A valid temporary fix would be removing SkiaSharp.NativeAssets.macOS as transitive package.

But I'm not an expert for nuget packages:

<Project Sdk="Microsoft.NET.Sdk.Web">
	<PropertyGroup>
		<TargetFramework>net48</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="SkiaSharp" Version="2.88.6" ExcludeAssets="buildTransitive" />
		<PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.6" />
	</ItemGroup>
</Project>

image

Maybe the correct bug fix would be removing SkiaSharp.NativeAssets.macOS nuget package dependency for net462 in SkiaSharp.nuspec:

https://github.com/mono/SkiaSharp/blob/e2c5c86249621857107c779af0f79b4d06613766/nuget/SkiaSharp.nuspec#L33C1-L33C1

skeller1 avatar Oct 19 '23 13:10 skeller1

Same issue. I had to solve it as follows in my csproj:

<ItemGroup>
    <PackageReference Include="SkiaSharp" Version="2.88.8" GeneratePathProperty="true" EcludeAssets="buildTransitive" />
    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" GeneratePathProperty="true" ExcludeAssets="all" />
    <PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.8" GeneratePathProperty="true" ExcludeAssets="all"/>
    <PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.8" GeneratePathProperty="true" ExcludeAssets="all" />
</ItemGroup>

<!-- hack for SkiaSharp, so the native dlls go in the right place -->
<Import Project="$(PkgSkiaSharp_NativeAssets_Linux)\build\net462\SkiaSharp.NativeAssets.Linux.targets" Condition="'$(TargetFramework)' != ''" />
<Import Project="$(PkgSkiaSharp_NativeAssets_macOS)\build\net462\SkiaSharp.NativeAssets.macOS.targets" Condition="'$(TargetFramework)' != ''" />
<Import Project="$(PkgSkiaSharp_NativeAssets_Win32)\build\net462\SkiaSharp.NativeAssets.Win32.targets" Condition="'$(TargetFramework)' != ''" />

Then, in my code I had to tell .NET where to load the native dlls as follows:

First I made a class that set's up a DllImportResolver when needed:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

namespace SolidCP.Providers.OS
{
    public class SkiaSharp
    {
        public bool IsLinuxMusl
        {
            get
            {
                if (!OSInfo.IsLinux) return false;
                return OS.Shell.Default.Exec("ldd /bin/ls").OutputAndError().Result.Contains("musl");
            }
        }

        static readonly SkiaSharp Current = new SkiaSharp(); 

        static Dictionary<string, IntPtr> loadedNativeDlls = new Dictionary<string, IntPtr>();
        public IntPtr SkiaDllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
        {
            if (libraryName.Contains("SkiaSharp"))
            {
                lock (this)
                {
                    IntPtr dll;
                    if (loadedNativeDlls.TryGetValue(libraryName, out dll)) return dll;

                    var runtimeInformation = typeof(RuntimeInformation);
                    var runtimeIdentifier = (string?)runtimeInformation.GetProperty("RuntimeIdentifier")?.GetValue(null);
                    if (runtimeIdentifier == "linux-x64" && IsLinuxMusl) runtimeIdentifier = "linux-musl-x64";
                    runtimeIdentifier = runtimeIdentifier.Replace("linux-", "");
                    var currentDllPath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
                    string libraryFileName = libraryName;
                    if (!libraryFileName.EndsWith(".so")) libraryFileName += ".so";
                    if (!libraryFileName.StartsWith("lib")) libraryFileName = "lib" + libraryFileName;
                    var nativeDllPath = Path.Combine(currentDllPath, runtimeIdentifier, libraryFileName);

                    if (File.Exists(nativeDllPath))
                    {
                        // call NativeLibrary.Load via reflection, becuase it's not available in NET Standard
                        var nativeLibrary = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices");
                        var load = nativeLibrary.GetMethod("Load", new Type[] { typeof(string), typeof(Assembly), typeof(DllImportSearchPath?) });
                        dll = (IntPtr)load?.Invoke(null, new object[] { nativeDllPath, assembly, searchPath });
                        loadedNativeDlls.Add(libraryName, dll);

                        Console.WriteLine($"Loaded native library: {nativeDllPath}");

                        return dll;
                    }
                }
            }

            // Otherwise, fallback to default import resolver.
            return IntPtr.Zero;
        }

        static bool nativeSkiaDllLoaded = false;
        public static void LoadNativeDlls()
        {
            if (nativeSkiaDllLoaded) return;
            nativeSkiaDllLoaded = true;

            if (OSInfo.IsLinux)
            {
                // call NativeLibrary.SetDllImportResolver via reflection, becuase it's not available in NET Standard
                var nativeLibrary = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices");
                var dllImportResolver = Type.GetType("System.Runtime.InteropServices.DllImportResolver, System.Runtime.InteropServices");

                Assembly skiaSharp = AppDomain.CurrentDomain.GetAssemblies()
                    .FirstOrDefault(a => a.GetName().Name == "SkiaSharp");
                if (skiaSharp == null)
                {
                    skiaSharp = Assembly.Load("SkiaSharp");
                }
                var setDllImportResolver = nativeLibrary.GetMethod("SetDllImportResolver", new Type[] { typeof(Assembly), dllImportResolver });
                //var importResolverMethod = this.GetType().GetMethod(nameof(SkiaDllImportResolver));

                var skiaDllImportResolver = Delegate.CreateDelegate(dllImportResolver, Current, nameof(SkiaDllImportResolver));
                setDllImportResolver?.Invoke(null, new object[] { skiaSharp, skiaDllImportResolver });

                Console.WriteLine("Added SkiaSharp DllImportResolver");
            }
        }
    }
}

Then, always before you run SkiaSharp in your code call SkiaSharp.LoadNativeDlls()

In the above code, OS.Shell.Default.Exec("ldd /bin/ls").OutputAndError().Result.Contains("musl");, this is a call to one of my library methods, what it does it calls a new process "ldd" with arguments "/bin/ls" and checks if the output contains "musl". Thats how you check if you're running on a Linux with musl c library, in which case you need to load the linux-musl-x64 .so SkiaSharp native library. You have to replace that line in my code with a proper call to Process.Start and then examine it's output. Also the call to OSInfo.IsLinux you have to replace with RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);

Probably when you do a dotnet publish you don't need all this, but my project runs on both .NET FX and .NET Core and my project cannot use `dotnet publish´

simonegli8 avatar Apr 16 '24 22:04 simonegli8