Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
268 views
in Technique[技术] by (71.8m points)

nuget - How to package a multi-architecture .NET library that targets the Universal Windows Platform?

How do I package a Universal Windows Platform library written in C# that offers only architecture-dependent builds? For the sake of illustration, let's say that I have some architecture-specific code conditionally compiled in for each architecture (using #if ARM and equivalents).

To be clear, no AnyCPU build exists for my library - only x86, x64 and ARM.

An equivalent and potentially more common situation is one where I have a dependency on an external library that is only provided as architecture-specific builds (e.g. Win2D). To keep the context simple, let's assume there are no dependencies and only my own code is involved - the solution should reduce to the same thing either way.

This is a series of questions and answers that document my findings on the topic of modern NuGet package authoring, focusing especially on the changes introduced with NuGet 3. You may also be interested in some related questions:

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

This answer builds upon the principles of .NET Framework library packaging and the principles of Universal Windows Platform library packaging. Read the linked answers first to better understand the following.

I will assume that all your architecture-specific builds expose the same API surface, with only the implementation of those APIs differing.

The main complication with this scenario is that the build toolchain requires an AnyCPU assembly for compile-time reference resolution, even if this assembly is never used at runtime. Since your scenario does not have AnyCPU build output, we need to find a workaround. The concept that applies here is of reference assemblies - AnyCPU assemblies only used at compile-time for reference validation. Therefore, to publish your library, you will need to create a reference assembly and package the assets as outlined below.

For simplicity, I will assume that your library has no dependencies on other NuGet packages. This is not likely to be the case in practice but dependency management is already covered by the other answers linked above and is therefore omitted from this answer.

The desired structure of the NuGet package is as follows:

+---ref
|   ---uap10.0
|       |   MultiArchitectureUwpLibrary.dll
|       |   MultiArchitectureUwpLibrary.pri
|       |   MultiArchitectureUwpLibrary.XML
|       |
|       ---MultiArchitectureUwpLibrary
|               ArchitectureControl.xaml
|               MultiArchitectureUwpLibrary.xr.xml
|
+---runtimes
|   +---win10-arm
|   |   ---lib
|   |       ---uap10.0
|   |               MultiArchitectureUwpLibrary.dll
|   |               MultiArchitectureUwpLibrary.pdb
|   |
|   +---win10-x64
|   |   ---lib
|   |       ---uap10.0
|   |               MultiArchitectureUwpLibrary.dll
|   |               MultiArchitectureUwpLibrary.pdb
|   |
|   ---win10-x86
|       ---lib
|           ---uap10.0
|                   MultiArchitectureUwpLibrary.dll
|                   MultiArchitectureUwpLibrary.pdb

If you have familiarized yourself with the answers linked above, the files should all be known to you already, although the directory structure is rather unusual in this case. The ref directory contains the reference assembly, the XML documentation and the resource files, while the architecture-specific assets are structured under the runtimes directory.

Most of this is pretty straightforward and can be accomplished by using a nuspec file created based on the following template:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
    <metadata minClientVersion="3.2">
        <id>Example.MultiArchitectureUwpLibrary</id>
        <version>1.0.0</version>
        <authors>Firstname Lastname</authors>
        <description>Example of library that is published as a set of architecture-specific assmeblies for the UWP platform.</description>
    </metadata>
    <files>
        <!-- Architecture-independent reference library for use at compile-time; generated by the PowerShell script. -->
        <file src="..inReferenceReleaseMultiArchitectureUwpLibrary.dll" target="refuap10.0" />

        <!-- XML documentation file goes together with the reference library. -->
        <file src="..inx86ReleaseMultiArchitectureUwpLibrary.xml" target="refuap10.0" />

        <!-- Resource files go together with the reference library. -->
        <file src="..inx86ReleaseMultiArchitectureUwpLibrary.pri" target="refuap10.0" />
        <file src="..inx86ReleaseMultiArchitectureUwpLibrary*" target="refuap10.0MultiArchitectureUwpLibrary" />

        <!-- The architecture-specific files go in architecture-specific directories. -->
        <file src="..inx86ReleaseMultiArchitectureUwpLibrary.dll" target="runtimeswin10-x86libuap10.0" />
        <file src="..inx86ReleaseMultiArchitectureUwpLibrary.pdb" target="runtimeswin10-x86libuap10.0" />

        <file src="..inx64ReleaseMultiArchitectureUwpLibrary.dll" target="runtimeswin10-x64libuap10.0" />
        <file src="..inx64ReleaseMultiArchitectureUwpLibrary.pdb" target="runtimeswin10-x64libuap10.0" />

        <file src="..inarmReleaseMultiArchitectureUwpLibrary.dll" target="runtimeswin10-armlibuap10.0" />
        <file src="..inarmReleaseMultiArchitectureUwpLibrary.pdb" target="runtimeswin10-armlibuap10.0" />
    </files>
</package>

The missing piece, of course, is the reference assembly. Thankfully, this is rather simple to solve - a reference assembly is an AnyCPU assembly that defines the same classes and methods that the runtime assemblies contain. Its main purpose is to provide the compiler a reference to work with, so the compiler can verify that all method calls are actually referencing methods that will exist at runtime. The actual code in the reference assembly (if any) is not used for anything.

Since all your architecture-specific builds expose the same API surface, we can simply take any one of them and instruct the compiler to use it as the reference assembly. The Windows SDK contains a utility named CorFlags.exe that can be used to convert an x86 assembly to an AnyCPU assembly, making this possible.

Below is a package creation script that creates the required reference assemblies before packaging the library. It assumes the Windows SDK is installed in the standard location. To understand the details of the logic, see the inline comments.

# Any assembly matching this filter will be transformed into an AnyCPU assembly.
$referenceDllFilter = "MultiArchitectureUwpLibrary.dll"

$programfilesx86 = "${Env:ProgramFiles(x86)}"
$corflags = Join-Path $programfilesx86 "Microsoft SDKsWindowsv10.0AinNETFX 4.6 Toolsx64CorFlags.exe"

If (!(Test-Path $corflags))
{
    Throw "Unable to find CorFlags.exe"
}

$solutionRoot = Resolve-Path ....
$topLevelDirectories = Get-ChildItem $solutionRoot -Directory
$binDirectories = $topLevelDirectories | %{ Get-ChildItem $_.FullName -Directory -Filter "bin" }

# Create reference assemblies, because otherwise the NuGet packages cannot be used.
# This creates them for all outputs that match the filter, in all output directories of all projects.
# It's a bit overkill but who cares - the process is very fast and keeps the script simple.
Foreach ($bin in $binDirectories)
{
    $x86 = Join-Path $bin.FullName "x86"
    $any = Join-Path $bin.FullName "Reference"

    If (!(Test-Path $x86))
    {
        Write-Host "Skipping reference assembly generation for $($bin.FullName) because it has no x86 directory."
        continue;
    }

    if (Test-Path $any)
    {
        Remove-Item -Recurse $any
    }

    New-Item $any -ItemType Directory
    New-Item "$anyRelease" -ItemType Directory

    $dlls = Get-ChildItem "$x86Release" -File -Filter $referenceDllFilter

    Foreach ($dll in $dlls)
    {
        Copy-Item $dll.FullName "$anyRelease"
    }

    $dlls = Get-ChildItem "$anyRelease" -File -Filter $referenceDllFilter

    Foreach ($dll in $dlls)
    {
        Write-Host "Converting to AnyCPU: $dll"

        & $corflags /32bitreq- $($dll.FullName)
    }
}

# Delete any existing output.
Remove-Item *.nupkg

# Create new packages for any nuspec files that exist in this directory.
Foreach ($nuspec in $(Get-Item *.nuspec))
{
    .NuGet.exe pack "$nuspec"
}

You may need to adjust the paths in the script to match the conventions used in your solution.

Run this script to create a NuGet package that enables your library to be used in all its architecture-specific variants! Remember to build your solution using the Release configuration for all the architectures before creating the NuGet package.

A sample library and the relevant packaging files are available on GitHub. The solution corresponding to this answer is MultiArchitectureUwpLibrary.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...