mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.52k stars 540 forks source link

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

Open heathdavies-eaton opened 1 year ago

heathdavies-eaton commented 1 year ago

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

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.

ArlenLi commented 1 year ago

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

heathdavies-eaton commented 1 year ago

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

skeller1 commented 1 year ago

@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

simonegli8 commented 7 months ago

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´

Shane32 commented 3 days ago

I am getting the same build error. My .NET Framework 4.8 web project referenced another local project, which referenced a nuget package, which referenced another nuget package, which referenced SkiaSharp. I can probably reproduce the issue in a sample repo if it would help.