2022-03-17 12:02:39 +03:00
//#define VERBOSE_COMPARISON
2020-05-29 18:58:34 +03:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text ;
using Xamarin.Utils ;
using NUnit.Framework ;
2022-12-01 23:57:36 +03:00
#nullable enable
2022-05-17 17:24:35 +03:00
2020-05-29 18:58:34 +03:00
namespace Xamarin.Tests {
public static class DotNet {
2022-12-01 23:57:36 +03:00
static string? dotnet_executable ;
2020-05-29 18:58:34 +03:00
public static string Executable {
get {
2022-12-01 23:57:36 +03:00
if ( dotnet_executable is null ) {
2022-03-21 17:56:57 +03:00
dotnet_executable = Configuration . GetVariable ( "DOTNET" , null ) ;
2020-05-29 18:58:34 +03:00
if ( string . IsNullOrEmpty ( dotnet_executable ) )
throw new Exception ( $"Could not find the dotnet executable." ) ;
if ( ! File . Exists ( dotnet_executable ) )
throw new FileNotFoundException ( $"The dotnet executable '{dotnet_executable}' does not exist." ) ;
}
return dotnet_executable ;
}
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertPack ( string project , Dictionary < string , string > ? properties = null )
2021-10-04 08:43:55 +03:00
{
return Execute ( "pack" , project , properties , true ) ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertPackFailure ( string project , Dictionary < string , string > ? properties = null )
2021-10-04 08:43:55 +03:00
{
var rv = Execute ( "pack" , project , properties , false ) ;
Assert . AreNotEqual ( 0 , rv . ExitCode , "Unexpected success" ) ;
return rv ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertPublish ( string project , Dictionary < string , string > ? properties = null )
2021-08-11 11:01:16 +03:00
{
return Execute ( "publish" , project , properties , true ) ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertPublishFailure ( string project , Dictionary < string , string > ? properties = null )
2021-11-30 01:13:48 +03:00
{
var rv = Execute ( "publish" , project , properties , false ) ;
Assert . AreNotEqual ( 0 , rv . ExitCode , "Unexpected success" ) ;
return rv ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertRestore ( string project , Dictionary < string , string > ? properties = null )
2022-07-12 10:37:01 +03:00
{
return Execute ( "restore" , project , properties , true ) ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult Restore ( string project , Dictionary < string , string > ? properties = null )
2022-08-23 16:03:30 +03:00
{
return Execute ( "restore" , project , properties , false ) ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertBuild ( string project , Dictionary < string , string > ? properties = null )
2020-05-29 18:58:34 +03:00
{
2020-10-27 17:25:44 +03:00
return Execute ( "build" , project , properties , true ) ;
2020-05-29 18:58:34 +03:00
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult AssertBuildFailure ( string project , Dictionary < string , string > ? properties = null )
2020-06-02 16:49:58 +03:00
{
2020-10-27 17:25:44 +03:00
var rv = Execute ( "build" , project , properties , false ) ;
2020-06-02 16:49:58 +03:00
Assert . AreNotEqual ( 0 , rv . ExitCode , "Unexpected success" ) ;
return rv ;
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult Build ( string project , Dictionary < string , string > ? properties = null )
2021-12-07 23:33:22 +03:00
{
return Execute ( "build" , project , properties , false ) ;
}
2022-12-22 14:04:36 +03:00
public static ExecutionResult AssertNew ( string outputDirectory , string template , string? name = null , string? language = null )
2021-07-14 18:58:31 +03:00
{
Directory . CreateDirectory ( outputDirectory ) ;
var args = new List < string > ( ) ;
args . Add ( "new" ) ;
args . Add ( template ) ;
2022-10-10 09:09:34 +03:00
if ( ! string . IsNullOrEmpty ( name ) ) {
args . Add ( "--name" ) ;
2022-12-01 23:57:36 +03:00
args . Add ( name ! ) ;
2022-10-10 09:09:34 +03:00
}
2021-07-14 18:58:31 +03:00
2022-12-22 14:04:36 +03:00
if ( ! string . IsNullOrEmpty ( language ) ) {
args . Add ( "--language" ) ;
args . Add ( language ) ;
}
2022-12-01 23:57:36 +03:00
var env = new Dictionary < string , string? > ( ) ;
2021-07-14 18:58:31 +03:00
env [ "MSBuildSDKsPath" ] = null ;
env [ "MSBUILD_EXE_PATH" ] = null ;
var output = new StringBuilder ( ) ;
var rv = Execution . RunWithStringBuildersAsync ( Executable , args , env , output , output , Console . Out , workingDirectory : outputDirectory , timeout : TimeSpan . FromMinutes ( 10 ) ) . Result ;
if ( rv . ExitCode ! = 0 ) {
Console . WriteLine ( $"'{Executable} {StringUtils.FormatArguments (args)}' failed with exit code {rv.ExitCode}." ) ;
Console . WriteLine ( output ) ;
Assert . AreEqual ( 0 , rv . ExitCode , $"Exit code: {Executable} {StringUtils.FormatArguments (args)}" ) ;
}
2022-12-01 23:57:36 +03:00
return new ExecutionResult ( output , output , rv . ExitCode ) ;
2021-07-14 18:58:31 +03:00
}
2022-12-01 23:57:36 +03:00
public static ExecutionResult Execute ( string verb , string project , Dictionary < string , string > ? properties , bool assert_success = true , string? target = null )
2020-05-29 18:58:34 +03:00
{
if ( ! File . Exists ( project ) )
throw new FileNotFoundException ( $"The project file '{project}' does not exist." ) ;
verb = verb . ToLowerInvariant ( ) ;
switch ( verb ) {
case "clean" :
case "build" :
2021-10-04 08:43:55 +03:00
case "pack" :
2021-08-11 11:01:16 +03:00
case "publish" :
2022-07-12 10:37:01 +03:00
case "restore" :
2020-05-29 18:58:34 +03:00
var args = new List < string > ( ) ;
args . Add ( verb ) ;
args . Add ( project ) ;
2022-12-01 23:57:36 +03:00
if ( properties is not null ) {
Dictionary < string , string > ? generatedProps = null ;
2021-05-21 23:29:08 +03:00
foreach ( var prop in properties ) {
if ( prop . Value . IndexOfAny ( new char [ ] { ';' } ) > = 0 ) {
// https://github.com/dotnet/msbuild/issues/471
// Escaping the semi colon like the issue suggests at one point doesn't work, because in
// that case MSBuild won't split the string into its parts for tasks that take a string[].
// This means that a task that takes a "string[] RuntimeIdentifiers" will get an array with
// a single element, where that single element is the whole RuntimeIdentifiers string.
// Example task: https://github.com/dotnet/sdk/blob/ffca47e9a36652da2e7041360f2201a2ba197194/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs#L45
2022-09-20 14:44:06 +03:00
// args.Add ($"/p:{prop.Key}=\"{prop.Value}\"");
// Setting a property with a semicolon from the command line doesn't work anymore.
// Ref: https://github.com/dotnet/sdk/issues/27059#issuecomment-1219319513
// So write these properties in a file instead. This is a behavioural difference, because
// they'll be project-specific instead of global, but I don't see a better workaround.
if ( generatedProps is null )
generatedProps = new Dictionary < string , string > ( ) ;
generatedProps . Add ( prop . Key , prop . Value ) ;
2021-05-21 23:29:08 +03:00
} else {
args . Add ( $"/p:{prop.Key}={prop.Value}" ) ;
}
}
2022-09-20 14:44:06 +03:00
if ( generatedProps is not null ) {
var sb = new StringBuilder ( ) ;
sb . AppendLine ( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" ) ;
sb . AppendLine ( "<Project>" ) ;
sb . AppendLine ( "\t<PropertyGroup>" ) ;
foreach ( var prop in generatedProps ) {
sb . AppendLine ( $"\t\t<{prop.Key}>{prop.Value}</{prop.Key}>" ) ;
}
sb . AppendLine ( "\t</PropertyGroup>" ) ;
sb . AppendLine ( "</Project>" ) ;
var generatedProjectFile = Path . Combine ( Cache . CreateTemporaryDirectory ( ) , "GeneratedProjectFile.props" ) ;
File . WriteAllText ( generatedProjectFile , sb . ToString ( ) ) ;
args . Add ( $"/p:GeneratedProjectFile={generatedProjectFile}" ) ;
}
2020-05-29 18:58:34 +03:00
}
2021-09-08 10:16:57 +03:00
if ( ! string . IsNullOrEmpty ( target ) )
args . Add ( "/t:" + target ) ;
2022-12-01 23:57:36 +03:00
var binlogPath = Path . Combine ( Path . GetDirectoryName ( project ) ! , $"log-{verb}-{DateTime.Now:yyyyMMdd_HHmmss}.binlog" ) ;
2020-10-27 17:25:44 +03:00
args . Add ( $"/bl:{binlogPath}" ) ;
2021-08-12 13:30:57 +03:00
Console . WriteLine ( $"Binlog: {binlogPath}" ) ;
2022-12-01 23:57:36 +03:00
var env = new Dictionary < string , string? > ( ) ;
2020-05-29 18:58:34 +03:00
env [ "MSBuildSDKsPath" ] = null ;
env [ "MSBUILD_EXE_PATH" ] = null ;
2020-06-02 16:49:58 +03:00
var output = new StringBuilder ( ) ;
2020-10-27 17:25:44 +03:00
var rv = Execution . RunWithStringBuildersAsync ( Executable , args , env , output , output , Console . Out , workingDirectory : Path . GetDirectoryName ( project ) , timeout : TimeSpan . FromMinutes ( 10 ) ) . Result ;
if ( assert_success & & rv . ExitCode ! = 0 ) {
2022-05-20 22:31:18 +03:00
var outputStr = output . ToString ( ) ;
2021-01-19 16:30:16 +03:00
Console . WriteLine ( $"'{Executable} {StringUtils.FormatArguments (args)}' failed with exit code {rv.ExitCode}." ) ;
2022-05-20 22:31:18 +03:00
Console . WriteLine ( outputStr ) ;
2020-10-27 17:25:44 +03:00
Assert . AreEqual ( 0 , rv . ExitCode , $"Exit code: {Executable} {StringUtils.FormatArguments (args)}" ) ;
2020-05-29 18:58:34 +03:00
}
2022-12-01 23:57:36 +03:00
return new ExecutionResult ( output , output , rv . ExitCode ) {
2020-10-27 17:25:44 +03:00
BinLogPath = binlogPath ,
2020-06-02 16:49:58 +03:00
} ;
2020-05-29 18:58:34 +03:00
default :
throw new NotImplementedException ( $"Unknown dotnet action: '{verb}'" ) ;
}
}
2020-09-29 09:44:08 +03:00
public static void CompareApps ( string old_app , string new_app )
{
2022-03-17 12:02:39 +03:00
#if VERBOSE_COMPARISON
2020-09-29 09:44:08 +03:00
Console . WriteLine ( $"Comparing:" ) ;
Console . WriteLine ( $" {old_app}" ) ;
Console . WriteLine ( $" {new_app}" ) ;
2022-03-17 12:02:39 +03:00
#endif
2020-09-29 09:44:08 +03:00
var all_old_files = Directory . GetFiles ( old_app , "*.*" , SearchOption . AllDirectories ) . Select ( ( v ) = > v . Substring ( old_app . Length + 1 ) ) ;
var all_new_files = Directory . GetFiles ( new_app , "*.*" , SearchOption . AllDirectories ) . Select ( ( v ) = > v . Substring ( new_app . Length + 1 ) ) ;
var filter = new Func < IEnumerable < string > , IEnumerable < string > > ( ( lst ) = > {
return lst . Where ( v = > {
var extension = Path . GetExtension ( v ) ;
switch ( extension ) {
case ".exe" :
case ".dll" :
case ".pdb" : // the set of BCL assemblies is quite different
return false ;
case ".dylib" : // ignore dylibs, they're not the same
return false ;
}
var filename = Path . GetFileName ( v ) ;
switch ( filename ) {
2021-04-10 00:06:26 +03:00
case "icudt.dat" :
return false ; // ICU data file only present on .NET
2020-11-11 09:43:49 +03:00
case "runtime-options.plist" :
return false ; // the .NET runtime will deal with selecting the http handler, no need for us to do anything
2021-06-16 16:22:02 +03:00
case "runtimeconfig.bin" :
return false ; // this file is present for .NET apps, but not legacy apps.
[msbuild] Only require a provisioning profile if we have non-empty entitlements. (#15918)
This is the behavior in legacy Xamarin (for mobile projects): if a project
contains a CodesignEntitlements=Entitlements.plist property, we require a
provisioning profile (and failing the build if none is found).
In .NET, the expected behavior is that if a file is in the project directory,
it should be detected automatically and handled accordingly. We didn't do this
for the initial release of .NET, but we implemented it later
(https://github.com/xamarin/xamarin-macios/pull/15729).
However, this turned out to be complicated, because many templates provide an
Entitlements.plist file, and now suddenly just the presence of such a file
would require a provisioning profile, which also means setting up the whole
rigmarole of Apple's certificates and provisioning profiles (and even
potentially getting a paid Apple Developer account).
This usually worked well in legacy Xamarin, because in templates only the
Release configuration would set the CodesignEntitlements=Entitlements.plist
property, and thus we'd only require a provisioning profile for release builds
(and the Entitlements.plist file would be ignored for Debug builds).
Here we change the default behavior when building for .NET so that we only
require a provisioning profile if the Entitlements.plist file is empty (i.e.
doesn't request any entitlements).
I've also implemented an override, where setting the
CodesignRequireProvisioningProfile=true property will override our default
logic.
Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1613459.
2022-09-13 11:51:41 +03:00
case "embedded.mobileprovision" :
case "CodeResources" :
return false ; // sometimes we don't sign in .NET when we do in legacy (if there's an empty Entitlements.plist file)
2020-09-29 09:44:08 +03:00
}
var components = v . Split ( '/' ) ;
if ( components . Any ( v = > v . EndsWith ( ".framework" , StringComparison . Ordinal ) ) ) {
2020-11-23 11:44:03 +03:00
return false ; // This is Mono.framework, which is waiting for https://github.com/dotnet/runtime/issues/42846
2020-09-29 09:44:08 +03:00
}
return true ;
} ) ;
} ) ;
2022-03-17 12:02:39 +03:00
var old_files = filter ( all_old_files ) ;
var new_files = filter ( all_new_files ) ;
var extra_old_files = old_files . Except ( new_files ) ;
var extra_new_files = new_files . Except ( old_files ) ;
#if VERBOSE_COMPARISON
2020-09-29 09:44:08 +03:00
Console . WriteLine ( "Files in old app:" ) ;
foreach ( var f in all_old_files . OrderBy ( v = > v ) )
Console . WriteLine ( $"\t{f}" ) ;
Console . WriteLine ( "Files in new app:" ) ;
foreach ( var f in all_new_files . OrderBy ( v = > v ) )
Console . WriteLine ( $"\t{f}" ) ;
Console . WriteLine ( "Files in old app (filtered):" ) ;
foreach ( var f in old_files . OrderBy ( v = > v ) )
Console . WriteLine ( $"\t{f}" ) ;
Console . WriteLine ( "Files in new app (filtered):" ) ;
foreach ( var f in new_files . OrderBy ( v = > v ) )
Console . WriteLine ( $"\t{f}" ) ;
if ( extra_new_files . Any ( ) ) {
Console . WriteLine ( "Extra dotnet files:" ) ;
foreach ( var f in extra_new_files )
Console . WriteLine ( $" {f}" ) ;
}
if ( extra_old_files . Any ( ) ) {
Console . WriteLine ( "Missing dotnet files:" ) ;
foreach ( var f in extra_old_files )
Console . WriteLine ( $" {f}" ) ;
}
// Print out a size comparison. A size difference does not fail the test, because some size differences are normal.
Console . WriteLine ( "Size comparison:" ) ;
foreach ( var file in new_files ) {
var new_size = new FileInfo ( Path . Combine ( new_app , file ) ) . Length ;
var old_size = new FileInfo ( Path . Combine ( old_app , file ) ) . Length ;
if ( new_size = = old_size )
continue ;
var diff = new_size - old_size ;
Console . WriteLine ( $"\t{file}: {old_size} bytes -> {new_size} bytes. Diff: {diff}" ) ;
}
var total_old = all_old_files . Select ( v = > new FileInfo ( Path . Combine ( old_app , v ) ) . Length ) . Sum ( ) ;
var total_new = all_new_files . Select ( v = > new FileInfo ( Path . Combine ( new_app , v ) ) . Length ) . Sum ( ) ;
var total_diff = total_new - total_old ;
Console . WriteLine ( ) ;
Console . WriteLine ( $"\tOld app size: {total_old} bytes = {total_old / 1024.0:0.0} KB = {total_old / (1024.0 * 1024.0):0.0} MB" ) ;
Console . WriteLine ( $"\tNew app size: {total_new} bytes = {total_new / 1024.0:0.0} KB = {total_new / (1024.0 * 1024.0):0.0} MB" ) ;
Console . WriteLine ( $"\tSize comparison complete, total size change: {total_diff} bytes = {total_diff / 1024.0:0.0} KB = {total_diff / (1024.0 * 1024.0):0.0} MB" ) ;
2022-03-17 12:02:39 +03:00
#endif
2020-09-29 09:44:08 +03:00
Assert . That ( extra_new_files , Is . Empty , "Extra dotnet files" ) ;
Assert . That ( extra_old_files , Is . Empty , "Missing dotnet files" ) ;
}
2020-05-29 18:58:34 +03:00
}
2020-06-02 16:49:58 +03:00
public class ExecutionResult {
public StringBuilder StandardOutput ;
public StringBuilder StandardError ;
public int ExitCode ;
public bool TimedOut ;
2020-10-27 17:25:44 +03:00
public string BinLogPath ;
2022-12-01 23:57:36 +03:00
public ExecutionResult ( StringBuilder stdout , StringBuilder stderr , int exitCode )
{
StandardOutput = stdout ;
StandardError = stderr ;
ExitCode = exitCode ;
BinLogPath = string . Empty ;
}
2020-06-02 16:49:58 +03:00
}
2020-05-29 18:58:34 +03:00
}