2020-09-24 15:57:05 +03:00
#pragma warning disable 0649 // Field 'X' is never assigned to, and will always have its default value null
2017-07-10 13:46:31 +03:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2017-11-20 16:55:16 +03:00
using System.Linq ;
2017-07-10 13:46:31 +03:00
using System.Text ;
using NUnit.Framework ;
2017-11-20 16:55:16 +03:00
using Mono.Cecil ;
using Mono.Cecil.Cil ;
2017-07-10 13:46:31 +03:00
using Xamarin.Utils ;
namespace Xamarin.Tests
{
class BGenTool : Tool
{
2020-09-24 15:57:05 +03:00
public const string None = "None" ;
2017-11-20 16:55:16 +03:00
AssemblyDefinition assembly ;
2017-07-10 13:46:31 +03:00
public Profile Profile ;
2019-03-19 20:06:02 +03:00
public bool InProcess = true ; // if executed using an in-process bgen. Ignored if using the classic bgen (for XM/Classic), in which case we'll always use the out-of-process bgen.
2017-11-13 19:54:21 +03:00
public bool ProcessEnums ;
2017-07-10 13:46:31 +03:00
public List < string > ApiDefinitions = new List < string > ( ) ;
2017-11-20 16:55:16 +03:00
public List < string > Sources = new List < string > ( ) ;
2018-05-04 19:02:39 +03:00
public List < string > References = new List < string > ( ) ;
2020-09-24 15:57:05 +03:00
// If BaseLibrary and AttributeLibrary are null, we calculate a default value
#if NET
public string BaseLibrary ;
public string AttributeLibrary ;
public bool ReferenceBclByDefault = true ;
#else
public string BaseLibrary = None ;
public string AttributeLibrary = None ;
public bool ReferenceBclByDefault = false ;
#endif
2017-11-20 16:55:16 +03:00
public string [ ] Defines ;
2017-07-10 13:46:31 +03:00
public string TmpDirectory ;
2017-11-16 18:02:34 +03:00
public string ResponseFile ;
2017-11-20 16:55:16 +03:00
public string WarnAsError ; // Set to empty string to pass /warnaserror, set to non-empty string to pass /warnaserror:<nonemptystring>
public string NoWarn ; // Set to empty string to pass /nowarn, set to non-empty string to pass /nowarn:<nonemptystring>
2019-04-11 09:34:36 +03:00
public string Out ;
2020-09-24 15:57:05 +03:00
public int Verbosity = 1 ;
2017-07-10 13:46:31 +03:00
2018-02-02 09:09:04 +03:00
protected override string ToolPath { get { return Profile = = Profile . macOSClassic ? Configuration . BGenClassicPath : Configuration . BGenPath ; } }
2017-07-10 13:46:31 +03:00
protected override string MessagePrefix { get { return "BI" ; } }
2018-02-02 09:09:04 +03:00
protected override string MessageToolName { get { return Profile = = Profile . macOSClassic ? "bgen-classic" : "bgen" ; } }
2017-07-10 13:46:31 +03:00
2017-11-20 16:55:16 +03:00
public BGenTool ( )
{
EnvironmentVariables = new Dictionary < string , string > {
{ "MD_MTOUCH_SDK_ROOT" , Configuration . SdkRootXI } ,
{ "XamarinMacFrameworkRoot" , Configuration . SdkRootXM } ,
} ;
}
public void AddTestApiDefinition ( string filename )
{
ApiDefinitions . Add ( Path . Combine ( Configuration . SourceRoot , "tests" , "generator" , filename ) ) ;
}
public AssemblyDefinition ApiAssembly {
get {
LoadAssembly ( ) ;
return assembly ;
}
}
2019-03-19 20:06:02 +03:00
string [ ] BuildArgumentArray ( )
2017-07-10 13:46:31 +03:00
{
2019-03-19 20:06:02 +03:00
var sb = new List < string > ( ) ;
2017-07-10 13:46:31 +03:00
var targetFramework = ( string ) null ;
2020-09-24 15:57:05 +03:00
#if NET
switch ( Profile ) {
case Profile . None :
break ;
case Profile . iOS :
2020-09-18 18:41:33 +03:00
targetFramework = TargetFramework . DotNet_6_0_iOS_String ;
2020-09-24 15:57:05 +03:00
break ;
case Profile . tvOS :
2020-09-18 18:41:33 +03:00
targetFramework = TargetFramework . DotNet_6_0_tvOS_String ;
2020-09-24 15:57:05 +03:00
break ;
case Profile . watchOS :
2020-09-18 18:41:33 +03:00
targetFramework = TargetFramework . DotNet_6_0_watchOS_String ;
2020-09-24 15:57:05 +03:00
break ;
case Profile . macOSMobile :
2020-09-18 18:41:33 +03:00
targetFramework = TargetFramework . DotNet_6_0_macOS_String ;
2020-09-24 15:57:05 +03:00
break ;
case Profile . macOSFull :
case Profile . macOSSystem :
throw new InvalidOperationException ( $"Only the Mobile profile can be specified for .NET" ) ;
default :
throw new NotImplementedException ( $"Profile: {Profile}" ) ;
}
#else
2017-07-10 13:46:31 +03:00
switch ( Profile ) {
case Profile . None :
break ;
case Profile . iOS :
targetFramework = "Xamarin.iOS,v1.0" ;
break ;
case Profile . tvOS :
targetFramework = "Xamarin.TVOS,v1.0" ;
break ;
case Profile . watchOS :
targetFramework = "Xamarin.WatchOS,v1.0" ;
break ;
2018-02-02 09:09:04 +03:00
case Profile . macOSClassic :
2017-07-10 13:46:31 +03:00
targetFramework = "XamMac,v1.0" ;
break ;
2018-02-02 09:09:04 +03:00
case Profile . macOSFull :
2017-07-10 13:46:31 +03:00
targetFramework = "Xamarin.Mac,Version=v4.5,Profile=Full" ;
break ;
2018-02-02 09:09:04 +03:00
case Profile . macOSMobile :
2017-07-10 13:46:31 +03:00
targetFramework = "Xamarin.Mac,Version=v2.0,Profile=Mobile" ;
break ;
2018-02-02 09:09:04 +03:00
case Profile . macOSSystem :
2017-07-10 13:46:31 +03:00
targetFramework = "Xamarin.Mac,Version=v4.5,Profile=System" ;
break ;
default :
throw new NotImplementedException ( $"Profile: {Profile}" ) ;
}
2020-09-24 15:57:05 +03:00
#endif
TargetFramework ? tf = null ;
if ( targetFramework ! = null )
tf = TargetFramework . Parse ( targetFramework ) ;
if ( BaseLibrary = = null ) {
if ( tf . HasValue )
sb . Add ( $"--baselib={Configuration.GetBaseLibrary (tf.Value)}" ) ;
} else if ( BaseLibrary ! = None ) {
sb . Add ( $"--baselib={BaseLibrary}" ) ;
}
if ( AttributeLibrary = = null ) {
if ( tf . HasValue )
sb . Add ( $"--attributelib={Configuration.GetBindingAttributePath (tf.Value)}" ) ;
} else if ( AttributeLibrary ! = None ) {
sb . Add ( $"--attributelib={AttributeLibrary}" ) ;
}
2017-07-10 13:46:31 +03:00
if ( ! string . IsNullOrEmpty ( targetFramework ) )
2019-03-19 20:06:02 +03:00
sb . Add ( $"--target-framework={targetFramework}" ) ;
2017-11-20 16:55:16 +03:00
2017-07-10 13:46:31 +03:00
foreach ( var ad in ApiDefinitions )
2019-03-19 20:06:02 +03:00
sb . Add ( $"--api={ad}" ) ;
2017-11-20 16:55:16 +03:00
foreach ( var s in Sources )
2019-03-19 20:06:02 +03:00
sb . Add ( $"-s={s}" ) ;
2017-11-20 16:55:16 +03:00
2020-09-24 15:57:05 +03:00
if ( ReferenceBclByDefault ) {
if ( tf = = null ) {
// do nothing
} else if ( tf . Value . IsDotNet = = true ) {
2021-03-09 16:57:56 +03:00
References . AddRange ( Directory . GetFiles ( Configuration . DotNet6BclDir , "*.dll" ) ) ;
2020-09-24 15:57:05 +03:00
} else {
throw new NotImplementedException ( "ReferenceBclByDefault" ) ;
}
}
2018-05-04 19:02:39 +03:00
foreach ( var r in References )
2019-03-19 20:06:02 +03:00
sb . Add ( $"-r={r}" ) ;
2018-05-04 19:02:39 +03:00
2017-07-10 13:46:31 +03:00
if ( ! string . IsNullOrEmpty ( TmpDirectory ) )
2019-03-19 20:06:02 +03:00
sb . Add ( $"--tmpdir={TmpDirectory}" ) ;
2017-07-10 13:46:31 +03:00
2017-11-16 18:02:34 +03:00
if ( ! string . IsNullOrEmpty ( ResponseFile ) )
2019-03-19 20:06:02 +03:00
sb . Add ( $"@{ResponseFile}" ) ;
if ( ! string . IsNullOrEmpty ( Out ) )
sb . Add ( $"--out={Out}" ) ;
2017-11-16 18:02:34 +03:00
2017-11-13 19:54:21 +03:00
if ( ProcessEnums )
2019-03-19 20:06:02 +03:00
sb . Add ( "--process-enums" ) ;
2017-11-13 19:54:21 +03:00
2017-11-20 16:55:16 +03:00
if ( Defines ! = null ) {
foreach ( var d in Defines )
2019-03-19 20:06:02 +03:00
sb . Add ( $"-d={d}" ) ;
2017-11-20 16:55:16 +03:00
}
if ( WarnAsError ! = null ) {
2019-03-19 20:06:02 +03:00
var arg = "--warnaserror" ;
2017-11-20 16:55:16 +03:00
if ( WarnAsError . Length > 0 )
2019-03-19 20:06:02 +03:00
arg + = ":" + WarnAsError ;
sb . Add ( arg ) ;
2017-11-20 16:55:16 +03:00
}
if ( NoWarn ! = null ) {
2019-03-19 20:06:02 +03:00
var arg = "--nowarn" ;
2017-11-20 16:55:16 +03:00
if ( NoWarn . Length > 0 )
2019-03-19 20:06:02 +03:00
arg + = ":" + NoWarn ;
sb . Add ( arg ) ;
2017-11-20 16:55:16 +03:00
}
2020-09-24 15:57:05 +03:00
if ( Verbosity ! = 0 )
sb . Add ( "-" + new string ( Verbosity > 0 ? 'v' : 'q' , Math . Abs ( Verbosity ) ) ) ;
2019-03-19 20:06:02 +03:00
return sb . ToArray ( ) ;
2017-07-10 13:46:31 +03:00
}
public void AssertExecute ( string message )
{
2019-03-19 20:06:02 +03:00
Assert . AreEqual ( 0 , Execute ( ) , message ) ;
2017-07-10 13:46:31 +03:00
}
public void AssertExecuteError ( string message )
{
2019-03-19 20:06:02 +03:00
Assert . AreNotEqual ( 0 , Execute ( ) , message ) ;
}
int Execute ( )
{
var arguments = BuildArgumentArray ( ) ;
var in_process = InProcess & & Profile ! = Profile . macOSClassic ;
if ( in_process ) {
2019-04-11 09:35:21 +03:00
int rv ;
2019-05-31 00:19:26 +03:00
var previous_environment = new Dictionary < string , string > ( ) ;
foreach ( var kvp in EnvironmentVariables ) {
previous_environment [ kvp . Key ] = Environment . GetEnvironmentVariable ( kvp . Key ) ;
Environment . SetEnvironmentVariable ( kvp . Key , kvp . Value ) ;
}
2019-04-11 09:35:21 +03:00
ThreadStaticTextWriter . ReplaceConsole ( Output ) ;
try {
rv = BindingTouch . Main ( arguments ) ;
} finally {
ThreadStaticTextWriter . RestoreConsole ( ) ;
2019-05-31 00:19:26 +03:00
foreach ( var kvp in previous_environment ) {
Environment . SetEnvironmentVariable ( kvp . Key , kvp . Value ) ;
}
2019-04-11 09:35:21 +03:00
}
2019-03-19 20:06:02 +03:00
Console . WriteLine ( Output ) ;
ParseMessages ( ) ;
return rv ;
}
2019-10-14 17:18:46 +03:00
return Execute ( arguments , always_show_output : true ) ;
2017-07-10 13:46:31 +03:00
}
2017-11-20 16:55:16 +03:00
public void AssertApiCallsMethod ( string caller_namespace , string caller_type , string caller_method , string @called_method , string message )
{
var type = ApiAssembly . MainModule . GetType ( caller_namespace , caller_type ) ;
var method = type . Methods . First ( ( v ) = > v . Name = = caller_method ) ;
AssertApiCallsMethod ( method , called_method , message ) ;
}
public void AssertApiCallsMethod ( MethodReference method , string called_method , string message )
{
var instructions = method . Resolve ( ) . Body . Instructions ;
foreach ( var ins in instructions ) {
if ( ins . OpCode . FlowControl ! = FlowControl . Call )
continue ;
var mr = ins . Operand as MethodReference ;
if ( mr = = null )
continue ;
if ( mr . Name = = called_method )
return ;
}
Assert . Fail ( $"Could not find any instructions calling {called_method} in {method.FullName}: {message}\n\t{string.Join (" \ n \ t ", instructions)}" ) ;
}
public void AssertApiLoadsField ( string caller_type , string caller_method , string declaring_type , string field , string message )
{
var type = ApiAssembly . MainModule . GetTypes ( ) . First ( ( v ) = > v . FullName = = caller_type ) ;
var method = type . Methods . First ( ( v ) = > v . Name = = caller_method ) ;
AssertApiLoadsField ( method , declaring_type , field , message ) ;
}
public void AssertApiLoadsField ( MethodReference method , string declaring_type , string field , string message )
{
var instructions = method . Resolve ( ) . Body . Instructions ;
foreach ( var ins in instructions ) {
if ( ins . OpCode . Code ! = Code . Ldsfld & & ins . OpCode . Code ! = Code . Ldfld )
continue ;
var fr = ins . Operand as FieldReference ;
if ( fr = = null )
continue ;
if ( fr . DeclaringType . FullName ! = declaring_type )
continue ;
if ( fr . Name = = field )
return ;
}
Assert . Fail ( $"Could not find any instructions loading the field {declaring_type}.{field} in {method.FullName}: {message}\n\t{string.Join (" \ n \ t ", instructions)}" ) ;
}
public void AssertPublicTypeCount ( int count , string message = null )
{
LoadAssembly ( ) ;
var actual = assembly . MainModule . Types . Where ( ( v ) = > v . IsPublic | | v . IsNestedPublic ) ;
if ( actual . Count ( ) ! = count )
Assert . Fail ( $"Expected {count} public type(s), found {actual} public type(s). {message}\n\t{string.Join (" \ n \ t ", actual.Select ((v) => v.FullName).ToArray ())}" ) ;
}
public void AssertPublicMethodCount ( string typename , int count , string message = null )
{
LoadAssembly ( ) ;
var t = assembly . MainModule . Types . FirstOrDefault ( ( v ) = > v . FullName = = typename ) ;
var actual = t . Methods . Count ( ( v ) = > {
if ( v . IsPrivate | | v . IsFamily | | v . IsFamilyAndAssembly )
return false ;
return true ;
} ) ;
if ( actual ! = count )
Assert . Fail ( $"Expected {count} publicly accessible method(s) in {typename}, found {actual} publicly accessible method(s). {message}" ) ;
}
public void AssertType ( string fullname , TypeAttributes ? attributes = null , string message = null )
{
LoadAssembly ( ) ;
var allTypes = assembly . MainModule . GetTypes ( ) . ToArray ( ) ;
var t = allTypes . FirstOrDefault ( ( v ) = > v . FullName = = fullname ) ;
if ( t = = null )
Assert . Fail ( $"No type named '{fullname}' in the generated assembly. {message}\nList of types:\n\t{string.Join (" \ n \ t ", allTypes.Select ((v) => v.FullName))}" ) ;
if ( attributes ! = null )
Assert . AreEqual ( attributes . Value , t . Attributes , $"Incorrect attributes for type {fullname}." ) ;
}
public void AssertMethod ( string typename , string method , string returnType = null , params string [ ] parameterTypes )
{
AssertMethod ( typename , method , null , returnType , parameterTypes ) ;
}
public void AssertMethod ( string typename , string method , MethodAttributes ? attributes = null , string returnType = null , params string [ ] parameterTypes )
{
LoadAssembly ( ) ;
var t = assembly . MainModule . Types . First ( ( v ) = > v . FullName = = typename ) ;
var m = t . Methods . FirstOrDefault ( ( v ) = > {
if ( v . Name ! = method )
return false ;
if ( v . Parameters . Count ! = parameterTypes . Length )
return false ;
for ( int i = 0 ; i < v . Parameters . Count ; i + + )
if ( v . Parameters [ i ] . ParameterType . FullName ! = parameterTypes [ i ] )
return false ;
return true ;
} ) ;
if ( m = = null )
Assert . Fail ( $"No method '{method}' with signature '{string.Join (" ' , ' ", parameterTypes)}' was found." ) ;
if ( attributes . HasValue )
Assert . AreEqual ( attributes . Value , m . Attributes , "Attributes for {0}" , m . FullName ) ;
}
void LoadAssembly ( )
{
if ( assembly = = null )
2019-04-11 09:34:36 +03:00
assembly = AssemblyDefinition . ReadAssembly ( Out ? ? ( Path . Combine ( TmpDirectory , Path . GetFileNameWithoutExtension ( ApiDefinitions [ 0 ] ) . Replace ( '-' , '_' ) + ".dll" ) ) ) ;
2017-11-20 16:55:16 +03:00
}
2017-07-10 13:46:31 +03:00
void EnsureTempDir ( )
{
if ( TmpDirectory = = null )
TmpDirectory = Cache . CreateTemporaryDirectory ( ) ;
}
2017-11-20 16:55:16 +03:00
public void CreateTemporaryBinding ( params string [ ] api_definition )
2017-07-10 13:46:31 +03:00
{
EnsureTempDir ( ) ;
2017-11-20 16:55:16 +03:00
for ( int i = 0 ; i < api_definition . Length ; i + + ) {
var api = Path . Combine ( TmpDirectory , $"api{i}.cs" ) ;
File . WriteAllText ( api , api_definition [ i ] ) ;
ApiDefinitions . Add ( api ) ;
}
2017-07-10 13:46:31 +03:00
WorkingDirectory = TmpDirectory ;
2019-04-11 09:34:36 +03:00
Out = Path . Combine ( WorkingDirectory , "api0.dll" ) ;
2017-07-10 13:46:31 +03:00
}
2017-11-20 16:55:16 +03:00
public static string [ ] GetDefaultDefines ( Profile profile )
{
switch ( profile ) {
2018-02-02 09:09:04 +03:00
case Profile . macOSFull :
case Profile . macOSMobile :
case Profile . macOSSystem :
2017-11-20 16:55:16 +03:00
return new string [ ] { "MONOMAC" , "XAMCORE_2_0" } ;
2018-02-02 09:09:04 +03:00
case Profile . macOSClassic :
2017-11-20 16:55:16 +03:00
return new string [ ] { "MONOMAC" } ;
case Profile . iOS :
return new string [ ] { "IOS" , "XAMCORE_2_0" } ;
default :
throw new NotImplementedException ( profile . ToString ( ) ) ;
}
}
2017-07-10 13:46:31 +03:00
}
2019-04-11 09:35:21 +03:00
// This class will replace stdout/stderr with its own thread-static storage for stdout/stderr.
// This means we're capturing stdout/stderr per thread.
class ThreadStaticTextWriter : TextWriter
{
[ThreadStatic]
static TextWriter current_writer ;
static ThreadStaticTextWriter instance = new ThreadStaticTextWriter ( ) ;
static object lock_obj = new object ( ) ;
static int counter ;
static TextWriter original_stdout ;
static TextWriter original_stderr ;
public static void ReplaceConsole ( StringBuilder sb )
{
lock ( lock_obj ) {
if ( counter = = 0 ) {
original_stdout = Console . Out ;
original_stderr = Console . Error ;
Console . SetOut ( instance ) ;
Console . SetError ( instance ) ;
}
counter + + ;
current_writer = new StringWriter ( sb ) ;
}
}
public static void RestoreConsole ( )
{
lock ( lock_obj ) {
current_writer . Dispose ( ) ;
current_writer = null ;
counter - - ;
if ( counter = = 0 ) {
Console . SetOut ( original_stdout ) ;
Console . SetError ( original_stderr ) ;
original_stdout = null ;
original_stderr = null ;
}
}
}
ThreadStaticTextWriter ( )
{
}
public TextWriter CurrentWriter {
get {
2021-09-20 14:39:24 +03:00
lock ( lock_obj ) {
if ( current_writer = = null )
return original_stdout ;
return current_writer ;
}
2019-04-11 09:35:21 +03:00
}
}
public override Encoding Encoding = > Encoding . UTF8 ;
public override void WriteLine ( )
{
lock ( lock_obj )
CurrentWriter . WriteLine ( ) ;
}
public override void Write ( char value )
{
lock ( lock_obj )
CurrentWriter . Write ( value ) ;
}
public override void Write ( string value )
{
lock ( lock_obj )
CurrentWriter . Write ( value ) ;
}
public override void Write ( char [ ] buffer )
{
lock ( lock_obj )
CurrentWriter . Write ( buffer ) ;
}
public override void WriteLine ( string value )
{
lock ( lock_obj )
CurrentWriter . WriteLine ( value ) ;
}
}
2017-07-10 13:46:31 +03:00
}