Una buena característica que se puede añadir a una librería además de SourceLink o crear la documentación es configurar la compilación como determinista.
Esto quiere decir que compilar ensamblados bajo las mismas condiciones de entrada producirá el mismo binario equivalente byte a byte. Por condiciones de entrada se entienden:
- La secuencia de parámetros de entrada al compilador (flags).
- Los contenidos del archivo de respuesta del compilador .rsp.
- La versión exacta del compilador usado así como de los ensamblados referenciados.
- Ruta completa del directorio actual (se pueden abreviar usando relativas)
- Contenidos binarios de todos los ficheros pasados al compilador
- Código fuente
- Ensamblados referenciados
- Módulos referenciados
- Recursos
- El nombre del fichero de claves (strong name)
- Archivos de respuesta @
- Analizadores
- Conjuntos de reglas
- «archivos adicionales» que puedan ser usados por analizadores
- La cultura actual (se refiere a la cultura en la que se producen los mensajes de excepción y diagnóstico)
- La codificación por defecto (o la actual) si la codificación no se ha especificado
- La existencia, o no existencia, y contenidos de archivos en las rutas de búsqueda del compilador ( p.ej /lib, /recurse)
- La plataforma CLR en la cual el compilador funciona (esto se nota por ejemplo al calcular precisiones en algunos tipos numéricos)
- El valor de %LIBPATH% ya que esto puede afectar la carga del analizador de dependencias
- Ahora mismo el compilador también depende de la hora del día y números aleatorios generados para GUID que no es determinístico si no se especifica /deterministic.
Permitir que el binario resultante sea el mismo proporciona varias ventajas por ejemplo para mecanismos de caché y optimización de test. También asegura que tanto la herramienta de compilación como la forma en que se ha compilado es consistente y verificable tanto por si misma con respecto al código fuente utilizado.
De igual forma en compilaciones incrementales incrementa la rapidez de compilación al compilar solo algunas partes, por ejemplo los ficheros de salida que ya están actualizados respecto a los ficheros de entrada no son ejecutados.
En el mundo DevOps proporciona ciertas ventajas para saber que pasos de compilación que dependen de cambios en un binario tienen que ser ejecutados.
La opción determinista está habilitada por defecto desde VisualStudio 2017, si se quiere habilitar para una versión anterior o deshabilitarla, tendríamos que editar el fichero .csproj.
<PropertyGroup>
<Deterministic>true</Deterministic>
</PropertyGroup>
Es posible que se quiera deshabilitar ya que la opción determinista no permite establecer un número de versión autoincremental (*) por razones obvias este valor cambia en cada compilación por lo que no es determinista.
Este valor también se puede pasar al compilador a través del flag /deterministic
DevOps
Los PDBs contienen las rutas de los ficheros que se usarán para depurar, esto en entornos locales no es un problema, pero en entornos DevOps puede ser un problema al no tener acceso a esas rutas o incluso a la máquina, para esto existe la directiva <DeterministicSourcePaths/> que debe ser establecida en true, esto hace que la compilación sea completamente determinista tanto en local como en entornos DevOps.
Configuración
Con todo lo anterior lo que habría que hacer es establecer las directivas <Deterministic/> y <ContinuousIntegrationBuild/> con los valores comentados.
Además hay que incluir la directiva <EmbedUntrackedSources /> para que se incluya también el código generado por el compilador como AssemblyInfo.cs, hay que tener en cuenta que si ya está configurado para SourceLink entonces esta directiva ya estará incluida.
Además para entornos DevOps hay que añadir una configuración adicional.
Azure DevOps
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
<Deterministic>True</Deterministic>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
GitHub
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<Deterministic>True</Deterministic>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
Si se está usando un target de SDK inferior a 3.1.300 hará falta agregar un fichero Directory.Build.targets con la siguiente configuración, también hará falta para librerías .NetStandard (por lo menos hasta la versión 2.1 incluida).
<Project>
<PropertyGroup>
<TargetFrameworkMonikerAssemblyAttributesPath>
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))
</TargetFrameworkMonikerAssemblyAttributesPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
</ItemGroup>
</Project>
Si se está usando Coverlet entonces habrá que usar la siguiente configuración para no tener problemas durante la integración continua.
<Project>
<PropertyGroup> <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
</ItemGroup>
<ItemGroup>
<SourceRoot Include="$(NuGetPackageRoot)" />
</ItemGroup>
<Target Name="CoverletGetPathMap"
DependsOnTargets="InitializeSourceRootMappedPaths"
Returns="@(_LocalTopLevelSourceRoot)"
Condition="'$(DeterministicSourcePaths)' == 'true'">
<ItemGroup>
<_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
</ItemGroup>
</Target>
</Project>
Mapear rutas
Si no se compila en formato portable es posible que las rutas dentro de los ficheros PDB no sean correctas o cambien, es posible mapear la ruta física respecto a la ruta que aparece en los PDB mediante la directiva PathMap, como:
<PropertyGroup>
<Deterministic>true</Deterministic>
<PathMap>$(EnlistmentRoot)=C:\</Features>
</PropertyGroup>
Siendo EnlistmentRoot la variable que apunta al raíz del código fuente, el parámetro del compilador sería:
-pathmap:path1=sourcePath1,path2=sourcePath2
Verificar
Para probarlo localmente se puede compilar pasando la propiedad TF_BUILD al compilador (si es orientado a Azure DevOps), por ejemplo:
dotnet build /p:TF_BUILD=true
Para comprobar que todo está correcto, después de compilar empaquetamos, usando por ejemplo la opción pack del proyecto y al abrirlo con NuGet Package Explorer deberíamos ver el tick verde en Deterministic.

Esto tiene una ventana añadida, podemos verificar que el paquete es correcto antes de aplicarlo a un pipe DevOps.
Referencias
Ventajas de compilaciones deterministas
Compilaciones deterministas en Roslyn
Deterministic source paths
Compilaciones incrementales
Entradas deterministas
Problemas con versionado automático
Configuración Deterministic Build
Configuración PathMap
Opción del compilador (-pathmap)
Opción del compilador (-deterministic)
Portable PDB






































