dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.67k stars 1.06k forks source link

Sign assemblies and publish in build pipeline #10891

Open Crossbow78 opened 4 years ago

Crossbow78 commented 4 years ago

Using .NET Core SDK 3.1.

We're trying to publish our client application with strong-named assemblies, as a single-file application.

So we setup an Azure Build Pipeline with the following outline:

The --no-build is causing issues:

C:\Program Files\dotnet\sdk\3.1.200\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(154,5): error MSB3030: Could not copy the file "(...)MyApp\bin\Debug\netcoreapp3.1\win-x64\MyApp.deps.json" because it was not found. [MyApp.csproj]

If we omit the --no-build flag everything succeeds, but it also rebuilds a fresh dll which is not signed.

You might suggest that we should first publish, and then apply the strong-name signing on the published output. But since the publish step produces a single-file application, we must sign our assemblies before they are wrapped into that single-file container... so how is this supposed to work? And why is --no-build behaving so weirdly?

dasMulli commented 4 years ago

Are you passing any other arguments to build and publish, esp. regarding runtime identifier? (e.g. not passing -r win-x64 to dotnet builld but to dotnet publish --no-build)

Crossbow78 commented 4 years ago

Yes, only the publish command has more arguments, and looks like this: dotnet publish MyApp.Client.csproj --no-build -r win-x64 (in its basic form, I omitted any specific parameters like PublishSingleFile)

And this is included in the project file: <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>

dasMulli commented 4 years ago

So does it change if you add the -r win-x64 to the dotnet build command as well? This is the part that produces the contents in bin\Debug\netcoreapp3.1\win-x64 that the subsequent publish would already expect to be there.

Crossbow78 commented 4 years ago

Here's two more observations:

(1) When I replace that line in the project file with the singular form (and I suppose that's the equivalent of the command line option): <RuntimeIdentifier>win-x64</RuntimeIdentifier> the publish step succeeds! I also noticed that the build output is then indeed placed under a win-x64 subfolder, which is what the publish step was looking for. Still leaves me wondering how to publish for multiple different runtimes.

(2) I was signing bin\Debug\netcoreapp3.1\win-x64\MyApp.Client.dll which is overwritten during the publish (even when using --no-build). But when I sign the one under obj instead, that one seems to be 'reused' (and the --no-build option actually plays a role there) and will successfully end up into the published output.

I'm not entirely sure whether all of this is obvious (or even expected) behavior...

dasMulli commented 4 years ago

The plural RuntimeIdentifiers in the cspros is mostly a no-op - it tells nuget to prepare assets for the RIDs (e.g. before we had implicit restore in 2.0+). It is not needed any more. And it does not affect any build or publish behaviour on its own. The singular RuntimeIdentifier is the same as passing -r to the CLI.

The assembly published will be the one in obj/[Configuration]/[TargetFramework]/[RuntimeIdentifier] for the project being published and in bin/... of the respective library folders.

Crossbow78 commented 4 years ago

Aha, that clears things up, thanks! I suppose I should sign the bin output for class library assemblies, and obj output for the application assemblies, even though it feels like relying on internal 'dotnet publish' behavior that could change any moment.

One last question, why does the build step require a runtime identifier at all? Does it compile differently when targeting different runtimes?

dasMulli commented 4 years ago

It mostly shouldn't matter, except for when it does.

Say you reference the EF Core SQLite provider and call dotnet bulid, you'll get a runtimes/ folder with all kinds of different native code for different platforms. If you pass -r win-x64, you'll only get the 64-bit windows dll and not the binaries for macOS or linux as well. NuGet packags can provide different implementations and even dependency grpahs for different runtimes ("bait-and-switch" packages).

Some project authors also choose to customize build settings for different RIDs by using a few msbuild condition inside the csproj (I recommend against that and relying on runtime checks wherever possible).

dasMulli commented 4 years ago

You could also trigger signing from within the build process itself.

E.g. create a file named Directory.Build.props in your project root directory (e.g. next to the .sln file) containing:

<Project>
  <PropertyGroup>
    <TargetsTriggeredByCompilation>$(SignIntermediateAssembly);SignIntermediateAssembly</TargetsTriggeredByCompilation>
  </PropertyGroup>

  <Target Name="SignIntermediateAssembly">
    <Exec Command="signtool.exe ... %(IntermediateAssembly.FullPath)" />
  </Target>

  <Target Name="SignPublishedSingleFileBundle" AfterTargets="BundlePublishDirectory">
    <Exec Command="signtool.exe ... $(PublishedSingleFilePath)" />
  </Target>
</Project>
Crossbow78 commented 4 years ago

Interesting, that Directory.Build.Props definitely feels like a cleaner way to achieve what I want.

In the pipeline I have the certificate's binary content sitting in pipeline variable after retrieving it from the keyvault, so I should probably dump it to disk before I could use signtool.

~Lastly, I'm currently using dotnet publish, but this build customization requires MSBuild right?~ Edit: I found that this target is automatically invoked even during dotnet build, which makes it fail when run during a local build (from within VS) since the certificate is not available there.

dasMulli commented 4 years ago

You can add an Condition="'$(SignAssemblies)' == 'True'" attribute to the Target elements and then pass -p:SignAssemblies=True as argument to dotnet build/publish if you only want to run that during CI.

Any pipeline variable should also be an environment variable during builds (or you can define extra env vars in the pipeline's YAML file). And all environment variables are automatically global MSBuild properties, thus you should be able to access pipeline variables in MSBuild using $(MY_VARIABLE) (replacing any dots with underscore).

jmecosta commented 2 years ago

Hi,

Do we have now out of the box support to sign dlls without workarounds (directory build.props) etc? at this point: we have "dotnet nuget sign nupkg" however the dlls remain unsigned... the current workflow is very weird to say the least:

  1. dotnet build
  2. sn.exe all dlls
  3. donet pack --no-build (here might be shooting myself in foot, since a rebuild might be issued and for example "dotnet msbuild bla --no-build" is not supported)

It should be possible to do: "dotnet build --sign --cert -timestamp" as we do for the nuget package... Alternatively my prefered approach, "dotnet nuget sign nupkg" should also sign the dlls

By the way, azure nuget packages everything is signed, nuget plus dlls... Is there some magic that can do all of it whiteout calling sn.exe?

PS: there is no publish step involved, its a client library. "dotnet build" should be enough build the packages and generate the signatures...

@sfoslund @dasMulli thanks in advance