项目作者: Mizux

项目描述 :
Test to build a multi-platforms Native NetStandard2.0 Nuget package using dotnet cli
高级语言: C++
项目地址: git://github.com/Mizux/dotnet-native.git
创建时间: 2018-08-08T14:44:45Z
项目社区:https://github.com/Mizux/dotnet-native

开源协议:Apache License 2.0

下载


Github-CI:
Build Status
Build Status
Build Status

Build Status

Introduction

This is an example of how to create a Modern CMake C++/.Net Project.

This project aim to explain how you build a .NetStandard2.0 native (win-x64,
linux-x64 and osx-x64) nuget multiple package using
.NET Core CLI and the
new .csproj format.

e.g. You have a cross platform C++ library (using a CMake based build) and a .NetStandard2.0 wrapper on it thanks to SWIG.
Then you want to provide a cross-platform Nuget package to consume it in a .NetCoreApp3.1 project…

Requirement

You’ll need:

  • “CMake >= 3.18”.
  • “.Net Core SDK >= 3.1” to get the dotnet cli.

note: We won’t/can’t rely on VS 2019 since we want a portable cross-platform dotnet/cli pipeline.

Codemap

The project layout is as follow:

Dependencies

To complexify a little, the CMake project is composed of three libraries (Foo, Bar and FooBar)
with the following dependencies:

  1. Foo:
  2. Bar:
  3. FooBar: PUBLIC Foo PRIVATE Bar

Build Process

To Create a native dependent package we will split it in two parts:

  • A bunch of Mizux.DotnetNative.runtime.{rid}.nupkg packages for each
    Runtime Identifier (RId) targeted containing the native libraries.
  • A generic package Mizux.DotnetNative.nupkg depending on each runtime packages and
    containing the managed .Net code.

Actually, You don’t need a specific variant of .Net Standard wrapper, simply omit the library extension and .Net magic will pick
the correct native library.
ref: https://www.mono-project.com/docs/advanced/pinvoke/#library-names

note: Microsoft.NetCore.App packages
follow this layout.

note: While Microsoft use runtime-<rid>.Company.Project for native libraries
naming, it is very difficult to get ownership on it, so you should prefer to use
Company.Project.runtime-<rid> instead since you can have ownership on
Company.* prefix more easily.

We have two use case scenario:

  1. Locally, be able to build a Mizux.DotnetNative package which only target the
    local OS Platform, i.e. building for only one
    Runtime Identifier (RID).
    \
    note: This is useful since the C++ build is a complex process for Windows,
    Linux and MacOS. i.e. We don’t support cross-compilation for the native
    library generation.

  2. Be able to create a complete cross-platform (ed. platform as multiple rid)
    Mizux.DotnetNative package. \
    i.e. First you generate each native Nuget package
    (Mizux.DotnetNative.runtime.{rid}.nupkg) on each native architecture, then
    copy paste these artifacts on one native machine to generate the
    meta-package Mizux.DotnetNative.

Local Mizux.DotnetNative Package

Let’s start with scenario 1: Create a Local only Mizux.DotnetNative.nupkg package targeting one
Runtime Identifier (RID).
We would like to build a Mizux.DotnetNative.nupkg package which only depends
on one Mizux.DotnetNative.runtime.{rid}.nupkg in order to work locally.

The pipeline for linux-x64 should be as follow:
Local Pipeline
Legend
note: The pipeline will be similar for osx-x64 and win-x64 architecture,
don’t hesitate to look at the CI log.

Building local runtime Mizux.DotnetNative Package

disclaimer: In this git repository, we use CMake and SWIG.
Thus we have the C++ shared library libFoo.so and the SWIG generated .Net wrapper Foo.cs.
note: For a C++ CMake cross-platform project sample, take a look at Mizux/cmake-cpp.
note: For a C++/Swig CMake cross-platform project sample, take a look at Mizux/cmake-swig.

So first let’s create the local Mizux.DotnetNative.runtime.{rid}.nupkg nuget package.

Here some dev-note concerning this Mizux.DotnetNative.runtime.{rid}.csproj.

  • Once you specify a RuntimeIdentifier then dotnet build or dotnet build -r {rid}
    will behave identically (save you from typing it).
    • note: it is NOT the case if you use RuntimeIdentifiers (notice the ‘s’)
  • It is recommended
    to add the tag native to the
    nuget package tags
    1. <PackageTags>native</PackageTags>
  • This package is a runtime package so we don’t want to ship an empty assembly file:
    1. <IncludeBuildOutput>false</IncludeBuildOutput>
  • Add the native (i.e. C++) libraries to the nuget package in the repository runtimes/{rid}/native. e.g. for linux-x64:
    1. <Content Include="*.so">
    2. <PackagePath>runtimes/linux-x64/native/%(Filename)%(Extension)</PackagePath>
    3. <Pack>true</Pack>
    4. <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    5. </Content>
  • Generate the runtime package to a defined directory (i.e. so later in
    Mizux.DotnetNative package we will be able to locate it)
    1. <PackageOutputPath>{...}/packages</PackageOutputPath>

Then you can generate the package using:

  1. dotnet pack Mizux.DotnetNative.runtime.{rid}

note: this will automatically trigger the dotnet build.

If everything good the package (located where your PackageOutputPath was defined) should have this layout:

  1. {...}/packages/Mizux.DotnetNative.runtime.{rid}.nupkg:
  2. \- Mizux.DotnetNative.runtime.{rid}.nuspec
  3. \- runtimes
  4. \- {rid}
  5. \- native
  6. \- *.so / *.dylib / *.dll
  7. ...

note: {rid} could be linux-x64 and {framework} could be netstandard2.0

tips: since nuget package are zip archive you can use unzip -l <package>.nupkg
to study their layout.

Building local Mizux.DotnetNative Package

So now, let’s create the local Mizux.DotnetNative.nupkg nuget package which will
depend on our previous runtime package.

Here some dev-note concerning this DotnetNative.csproj.

  • Add the previous package directory:
    1. <RestoreSources>{...}/packages;$(RestoreSources)</RestoreSources>
  • Add dependency (i.e. PackageReference) on each runtime package(s) availabe:
    1. <ItemGroup>
    2. <RuntimeLinux Include="{...}/packages/Mizux.DotnetNative.runtime.linux-x64.*.nupkg"></RuntimeLinux>
    3. <RuntimeOsx Include="{...}/packages/Mizux.DotnetNative.runtime.osx-x64.*.nupkg"></RuntimeOsx>
    4. <RuntimeWin Include="{...}/packages/Mizux.DotnetNative.runtime.win-x64.*.nupkg"></RuntimeWin>
    5. <PackageReference Include="Mizux.DotnetNative.runtime.linux-x64" Version="1.0" Condition="Exists('@(RuntimeLinux)')"></PackageReference>
    6. <PackageReference Include="Mizux.DotnetNative.runtime.osx-x64" Version="1.0" Condition="Exists('@(RuntimeOsx)')" ></PackageReference>
    7. <PackageReference Include="Mizux.DotnetNative.runtime.win-x64" Version="1.0" Condition="Exists('@(RuntimeWin)')" ></PackageReference>
    8. </ItemGroup>
    Thanks to the RestoreSource we can work locally we our just builded package
    without the need to upload it on nuget.org.

Then you can generate the package using:

  1. dotnet pack Mizux.DotnetNative

If everything good the package (located where your PackageOutputPath was
defined) should have this layout:

  1. {...}/packages/Mizux.DotnetNative.nupkg:
  2. \- Mizux.DotnetNative.nuspec
  3. \- lib
  4. \- {framework}
  5. \- Mizux.DotnetNative.dll
  6. ...

note: {framework} could be netcoreapp3.1 or/and net6.0

Testing local Mizux.DotnetNative.FooApp Package

We can test everything is working by using the Mizux.DotnetNative.FooApp or Mizux.DotnetNative.FooTests project.

First you can build it using:

  1. dotnet build <build_dir>/dotnet/FooApp

note: Since Mizux.DotnetNative.FooApp PackageReference Mizux.DotnetNative and add {...}/packages to the RestoreSource.
During the build of DotnetNative.FooApp you can see that Mizux.DotnetNative and
Mizux.DotnetNative.runtime.{rid} are automatically installed in the nuget cache.

Then you can run it using:

  1. dotnet run --project <build_dir>/dotnet/FooApp/FooApp.csproj

note: Contrary to dotnet build and dotnet pack you must use --project
before the .csproj path (let’s call it “dotnet cli command consistency”)

You should see:

  1. $ dotnet run --project build/dotnet/FooApp/FooApp.csproj
  2. [1] Enter DotnetNativeApp
  3. [2] Enter Foo::staticFunction(int)
  4. [3] Enter freeFunction(int)
  5. [3] Exit freeFunction(int)
  6. [2] Exit Foo::staticFunction(int)
  7. [1] Exit DotnetNativeApp

Complete Mizux.DotnetNative Package

Let’s start with scenario 2: Create a Complete Mizux.DotnetNative.nupkg package
targeting multiple
Runtime Identifier (RID).
We would like to build a Mizux.DotnetNative.nupkg package which depends on several
Mizux.DotnetNative.runtime.{rid}.nupkg.

The pipeline should be as follow:
note: This pipeline should be run on any architecture,
provided you have generated the three architecture dependent Mizux.DotnetNative.runtime.{rid}.nupkg
nuget packages.
Full Pipeline
Legend

Building All runtime Mizux.DotnetNative Package

Like in the previous scenario, on each targeted OS Platform you can build the
coresponding Mizux.DotnetNative.runtime.{rid}.nupkg package.

Simply run on each platform:

  1. cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release
  2. cmake --build build --config Release

note: replace {rid} by the Runtime Identifier associated to the current OS platform.

Then on one machine used, you copy all other packages in the {...}/packages so
when building Mizux.DotnetNative.csproj we can have access to all package…

Building Complete Mizux.DotnetNative Package

This is the same step than in the previous scenario, since we “see” all runtime
packages in {...}/packages, the project will depends on each of them.

Once copied all runtime package locally, simply run:

  1. dotnet build <build_dir>/dotnet/Mizux.DotnetNative
  2. dotnet pack <build_dir>/dotnet/Mizux.DotnetNative

Testing Complete Mizux.DotnetNative Package

We can test everything is working by using the Mizux.DotnetNative.FooApp or Mizux.DotnetNative.FooTests project.

First you can build it using:

  1. dotnet build <build_dir>/dotnet/FooApp

note: Since Mizux.DotnetNative.FooApp PackageReference Mizux.DotnetNative and add {...}/packages to the RestoreSource.
During the build of Mizux.DotnetNative.FooApp you can see that Mizux.DotnetNative and
Mizux.DotnetNative.runtime.{rid} are automatically installed in the nuget cache.

Then you can run it using:

  1. dotnet run --project <build_dir>/dotnet/FooApp/FooApp.csproj

You should see something like this

  1. $ dotnet run --project build/dotnet/FooApp/FooApp.csproj
  2. [1] Enter DotnetNativeApp
  3. [2] Enter Foo::staticFunction(int)
  4. [3] Enter freeFunction(int)
  5. [3] Exit freeFunction(int)
  6. [2] Exit Foo::staticFunction(int)
  7. [1] Exit DotnetNativeApp

Appendices

Few links on the subject…

.Net runtime can deduce library extension so don’t use a platform-specific
library name in the DllImport statement.
Instead, just use the library name itself, without any prefixes or suffixes,
and rely on the runtime to find the appropriate library at runtime.\
ref: Mono pinvoke#libraryname

Resources

Project layout

CMake

Target Framework Moniker (TFM)

.Net:Runtime IDentifier (RID)

Reference on .csproj format

Issues

Some issue related to this process

Misc

Image has been generated using plantuml:

  1. plantuml -Tsvg docs/{file}.dot

So you can find the dot source files in docs.

License

Apache 2. See the LICENSE file for details.

Disclaimer

This is not an official Google product, it is just code that happens to be
owned by Google.