Open bddckr opened 4 years ago
I've not done this before myself yet, and most examples I know about only call into different native libraries for each platform, but all of them share a common API already.
So I'm wondering if this is possible for SpiderEye - there needs to some smart handling/loading of a specific OS's .dll or the SpiderEye source needs to change to merge all OS-specific source projects and do some good old #ifdef
? The latter makes the development experience worse for maintaining SpiderEye for sure, so let's not aim for that 😅
The following .csproj
does add the various existing assemblies into a single NuGet package, which seems to work. I named it SpiderEye.Packaging
, so that's why the AssemblyName
gets adjusted before the import of SpiderEye.Shared.proj
, which then results in a package just called Bildstein.SpiderEye
.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>$(MSBuildProjectName.Replace('.Packaging', ''))</AssemblyName>
</PropertyGroup>
<Import Project="..\Shared\SpiderEye.Shared.proj" />
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DisableTransitiveFrameworkReferences>true</DisableTransitiveFrameworkReferences>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\**\*.csproj" />
<ProjectReference Remove="$(MSBuildProjectFullPath)" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)**/*.Core.*" Pack="true" PackagePath="ref/$(TargetFramework);runtimes/linux-x64/lib/$(TargetFramework);runtimes/osx-x64/lib/$(TargetFramework);runtimes/win/lib/$(TargetFramework)" />
<None Include="$(OutputPath)**/*.Linux.*" Pack="true" PackagePath="runtimes/linux-x64/lib/$(TargetFramework)" />
<None Include="$(OutputPath)**/*.Mac.*" Pack="true" PackagePath="runtimes/osx-x64/lib/$(TargetFramework)" />
<None Include="$(OutputPath)**/*.Windows.*" Pack="true" PackagePath="runtimes/win/lib/$(TargetFramework)" />
</ItemGroup>
</Project>
The downside to this approach: Each platform project's NuGet dependencies (at this time only the Windows project has one) isn't included properly. I can of course manually add a reference to the package in this new project, and there's probably also some MSBuild logic I can add to add those transitive package references as a direct one instead.
However, for now I'm looking into another approach instead: Keep the platform-specific NuGet packages and instead pack them all into a new "meta package" instead.
The bigger missing point for either approach is to get the APIs wrapped in a way that does the correct call to the right assembly at runtime. Still looking into that but using Core (or a new project) as the entry point, making that a reference assembly and that then calling into a manually loaded platform-specific assembly should work.
We do something like this (multiplatform bindings with single nuget) with our RocksDB bindings, if you want to check it out: https://github.com/curiosity-ai/rocksdb-sharp
Thanks for the suggestion and detailed explanation and examples!
I have tried various things to get a single Nuget package but nothing really worked so far. The main reasons I decide against it in the end (other than not working) was
The metapackage sounds like the most promising way to me but doesn't that mean that each platform specific build also contains the DLLs of all platforms?
Either way, I'm open to solutions for a single Nuget package as long as separate projects are still possible (for those that need it).
@theolivenbaum thank you for the example, always good to see something that works (the docs are a bit sparse on that topic). It's a bit different though from what I'd need here. You include native libs while I need to include .Net libs that should get referenced.
It's a bit different though from what I'd need here. You include native libs while I need to include .Net libs that should get referenced.
I think that's what I'm slowly able to confirm works fine for non-native assemblies, too: We just need to put the dlls into runtimes/$(Rid)/lib/$(TargetFramework)
rather than runtimes/$(Rid)/native
. The reason as to why this works is explained in the linked NuGet docs (emphasis mine):
Please note, NuGet always picks these compile or runtime assets from one folder so if there are some compatible assets from
/ref
then/lib
will be ignored to add compile-time assemblies. Similarly, if there are some compatible assets from/runtimes
then also/lib
will be ignored for runtime.
So once we provide our libs via the runtimes
folder we don't actually need anything in the root lib
as it will be ignored. That is, as long as we have one runtime
folder for each supported runtime identifier at least, otherwise it would fall back to the root lib
folder just for that runtime.
Note the first sentence of this quote: As soon as we have a single platform-specific assembly in its runtime
folder we also need to ensure that same folder has all the other assemblies we need. Ultimately this bloats the NuGet up a bit as the Core dll needs to be copied into all runtime folders. I think that's fine - it's not much, it compresses perfectly and more importantly it only matters on the developer machine.
The metapackage sounds like the most promising way to me but doesn't that mean that each platform specific build also contains the DLLs of all platforms?
I just finished some tests and it looks like NuGet only includes the dlls for the runtime(s) you build for. This matches the behavior of native libs from native
- the build output only has what it needs.
NuGet allows us to differentiate between the assemblies we want to reference at compile time, and which ones to reference at runtime:
These assemblies will only be available at runtime, so if you want to provide the corresponding compile time assembly as well then have
AnyCPU
assembly in/ref/{tfm}
folder.
So what this is saying is that we should put the assemblies we want to compile against into ref
, while we put all runtime ones into runtimes
. And it's just additionally smart about publishing for a runtime and only includes what you need from that list of supported runtimes from our NuGet package.
ref
is important here: We want the developer to work on any OS, so we need to ensure the code they compile against is platform-agnostic. That is already true for the Core project, so we can also include that in ref
. For this reason we should not include the OS-specific dlls in ref
, though.
Normally you'd make the Core project output a reference assembly, but in our case Core is already agnostic, so it's perfectly fine to put Core into ref
directly.
A good summary of all of this can be found here.
I just found a pretty good example of this solution! Microsoft.Data.SqlClient does what we want:
<None>
above from the other comments seems to work fine.pack
s.
https://github.com/dotnet/SqlClient/blob/7471403aac47ab724ae1885140f889c308ef0745/tools/targets/GenerateNugetPackage.targets#L10The nuspec has a few interesting things to it.
Compile
assets for any package it references that is not available on all OSs. At least I think that's the reason they exclude here. It might also just be a case of ensuring that the public API surface of this package does not add private assets only the package itself needs.
https://github.com/dotnet/SqlClient/blob/7471403aac47ab724ae1885140f889c308ef0745/tools/specs/Microsoft.Data.SqlClient.nuspec#L59-L69ExcludeAssets
allows us to set this./ref
).
https://github.com/dotnet/SqlClient/blob/7471403aac47ab724ae1885140f889c308ef0745/tools/specs/Microsoft.Data.SqlClient.nuspec#L115-L118<None>
here just fine./runtimes
).
https://github.com/dotnet/SqlClient/blob/7471403aac47ab724ae1885140f889c308ef0745/tools/specs/Microsoft.Data.SqlClient.nuspec#L166-L170<None>
for this.SpiderEye is a bit different when it comes to a few things: We might have an issue with this getting put in our auto-generated nuspec.
<frameworkReferences>
<group targetFramework=".NETCoreApp3.0">
<frameworkReference name="Microsoft.WindowsDesktop.App.WindowsForms" />
</group>
</frameworkReferences>
This is from the SpiderEye.Windows project, because it uses <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
and/or <UseWindowsForms>true</UseWindowsForms>
. To handle this we can set <DisableTransitiveFrameworkReferences>true</DisableTransitiveFrameworkReferences>
and leave it up to the user's project to do the following (see the docs):
<Project>
<PropertyGroup>
<TargetsWindows>false</TargetsWindows>
<TargetsWindows Condition="'$(RuntimeIdentifier)' != '' AND $(RuntimeIdentifier.StartsWith('win'))">true</TargetsWindows>
</PropertyGroup>
<Import Condition="!$(TargetsWindows)" Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<Import Condition="$(TargetsWindows)" Project="Sdk.props" Sdk="Microsoft.NET.Sdk.WindowsDesktop" />
<!-- The actual project configuration. -->
<Import Condition="!$(TargetsWindows)" Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Import Condition="$(TargetsWindows)" Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.WindowsDesktop" />
</Project>
The Windows project also uses <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.18362.2005" PrivateAssets="all" />
but that doesn't propagate to the nuspec and consumers anyway due to PrivateAssets="all"
.
So with all of this knowledge this is the tl;dr of the solution I think:
SpiderEye.Unified
project. This project references Core and the OS-specific projects and does all of the above steps via MSBuild configuration for us whenever we run its Pack
target (which dotnet pack
runs).<PackageReference>
s that are used in any of the referenced projects. Currently the only ones of interest are actually defined in Shared\SpiderEye.Shared.proj
so we can just <Import>
that file in the Unified project./runtimes
currently./ref
to continue supporting direct referencing from users who want OS-specific projects on their end./ref
folder gets merged from all referenced packages, so we would be polluting the new project's output consumption with OS-specific assemblies.The result is a new NuGet package that has Core in /ref
and each OS-specific output in /runtimes
(along with Core's output because NuGet only retrieves from one folder - the matching runtime folder).
Now the only missing piece is the need to define some API somewhere that users can call to initialize the Application
in an OS-agnostic way. We already added a new project - Unified, so let's add it there :smile: This project's assembly thus also needs to be put in /ref
and all runtime folders. We can use ~#ifdef
~ runtime checks in this one to call the right API.
Additionally I want to look into making our own custom SDK to make the above csproj setup a user has to do in their project a single line. I.e. <Project Sdk="SpiderEye.Sdk" />
.
I'm working on this over this weekend, we'll see if this works 😉
woah this is amazing :astonished:
I'm totally on board with SpiderEye.Unified
and adding an OS-agnostic application init class in there sounds like a good way to do it.
To the new project we have to manually add any
<PackageReference>
s that are used in any of the referenced projects. Currently the only ones of interest are actually defined inShared\SpiderEye.Shared.proj
so we can just<Import>
that file in the Unified project.
The windows project will soon need a reference to support the chromium edge webview (maybe macOS as well, I haven't looked into that yet). To avoid duplicated reference entries I think it'd be best to have a shared proj file for each platform that lists references and is then imported in both the actual OS project and the unified project. This way you can't forget to add references to both projects and version updates only have to be done once.
Having a custom SDK would be the icing on the cake. I didn't even know that it was possible to create a custom one, that's very cool. If you manage to get that working I'd love to add the bit for copying client files into the assembly as well:
<ItemGroup>
<EmbeddedResource Include="Angular\dist\**">
<LogicalName>%(RelativeDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
where the include path is configurable of course and maybe a flag to disable this as well.
Thank you so much for all that detective work, you got way farther than I did :smile: Keep me posted, this weekend I'll be working on some of the open issues and see how far I can get with chromium edge.
I ran into a few more issues, so it will be a bit until I can get this into a state that hopefully works. Then we can clean it up afterwards 😉
I'll update this issue once I can.
no worries, I didn't expect it to work without a fight :smile:
If you like you can create a WIP pull request if you need any input or a second set of eyes.
Describe the feature you'd like
Offer one NuGet package, that is multi-targeted and contains the necessary assemblies to support all supported OSs. Then additionally the
<OsName>Application.Init
API can be simplified to do the right call based on a runtime check. This would replace the current packages as it can be a full replacement for consumers of SpiderEye.Reason for this feature request
Currently projects that use SpiderEye on more than one target OS have to be developed with N+1 projects, where N is the number of the OSs. The reason for that is the fact that SpiderEye currently ships support for each OS in a separate NuGet. The remaining project acts as the "core" project that defines most of the logic, while the other projects all just configure the app on startup via the correct
<OsName>Application
API for that OS.With this change a user of SpiderEye would only add a single NuGet package to their project and call a single initialization method. Besides a bigger NuGet package for developers to download this would not result in unnecessary assembly additions for a specific target OS.
Besides simplification for consumers this change would most likely also