diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 475d8fae412..a64abd1d3bf 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -1053,8 +1053,7 @@ + > + + The assembly {0} does not provide an 'ObjectFile' metadata. + + Don't translate: 'ObjectFile'. + + + + + The assembly {0} was passed multiple times as an input assembly (referenced from {1}). + + {0}: the name of an assembly + {1}: the path to an assembly + + + + + Failed to AOT compile {0}, the AOT compiler exited with code {1}. + + {0}: the path to an assembly + {1}: the exit code of a process + + + + + Encountered an assembly reference cycle related to the assembly {0}. + + {0}: the path to an assembly + + diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/AOTCompileTaskBase.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/AOTCompileTaskBase.cs index 67e13be2bbf..c32ed8ebaca 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/AOTCompileTaskBase.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/AOTCompileTaskBase.cs @@ -7,6 +7,8 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Mono.Cecil; + using Xamarin.Localization.MSBuild; using Xamarin.Utils; @@ -42,11 +44,111 @@ public abstract class AOTCompileTaskBase : XamarinTask { public ITaskItem []? FileWrites { get; set; } #endregion + class AssemblyInfo { + public ITaskItem TaskItem; + public bool? IsUpToDate; + + public AssemblyInfo (ITaskItem item) + { + TaskItem = item; + } + } + + Dictionary assemblyInfos = new Dictionary (StringComparer.OrdinalIgnoreCase); + + string GetAssemblyName (ITaskItem item) + { + return Path.GetFileNameWithoutExtension (item.ItemSpec); + } + + bool IsUpToDate (ITaskItem assembly) + { + var assemblyPath = assembly.ItemSpec; + var key = GetAssemblyName (assembly); + if (assemblyInfos.TryGetValue (key, out var info)) { + if (!info.IsUpToDate.HasValue) { + Log.LogError (MSBStrings.E7119 /* Encountered an assembly reference cycle related to the assembly {0}. */, assemblyPath); + info.IsUpToDate = false; + return false; + } + return info.IsUpToDate.Value; + } + + info = new AssemblyInfo (assembly); + assemblyInfos [key] = info; + + var finfo = new FileInfo (assemblyPath); + if (!finfo.Exists) { + Log.LogError (MSBStrings.E0158 /* The file {0} does not exist. */, assemblyPath); + info.IsUpToDate = false; + return false; + } + + // ObjectFile is required + var objectFile = assembly.GetMetadata ("ObjectFile"); + if (string.IsNullOrEmpty (objectFile)) { + Log.LogError (MSBStrings.E7116 /* The assembly {0} does not provide an 'ObjectFile' metadata. */, assembly.ItemSpec); + info.IsUpToDate = false; + return false; + } + var objectFileInfo = new FileInfo (objectFile); + if (!IsUpToDate (finfo, objectFileInfo)) { + Log.LogMessage (MessageImportance.Low, "The assembly {0} is not up-to-date with regards to the object file {1}", assemblyPath, objectFile); + info.IsUpToDate = false; + return false; + } + + // LLVMFile is optional + var llvmFile = assembly.GetMetadata ("LLVMFile"); + if (!string.IsNullOrEmpty (llvmFile)) { + var llvmFileInfo = new FileInfo (llvmFile); + if (!IsUpToDate (finfo, llvmFileInfo)) { + Log.LogMessage (MessageImportance.Low, "The assembly {0} is not up-to-date with regards to the llvm file {1}", assemblyPath, llvmFile); + info.IsUpToDate = false; + return false; + } + } + + // We know now the assembly itself is up-to-date, but what about every referenced assembly? + // This assembly must be AOT-compiled again if any referenced assembly has changed as well. + using var ad = AssemblyDefinition.ReadAssembly (assembly.ItemSpec, new ReaderParameters { ReadingMode = ReadingMode.Deferred }); + foreach (var ar in ad.MainModule.AssemblyReferences) { + var referencedItems = Assemblies.Where (v => string.Equals (GetAssemblyName (v), ar.Name, StringComparison.OrdinalIgnoreCase)).ToArray (); + if (referencedItems.Length == 0) { + Log.LogMessage (MessageImportance.Low, $"Ignoring unresolved assembly {ar.Name} (referenced from {assemblyPath})."); + continue; + } else if (referencedItems.Length > 1) { + Log.LogError (MSBStrings.E7117 /* The assembly {0} was passed multiple times as an input assembly (referenced from {1}). */, ar.Name, assemblyPath); + info.IsUpToDate = false; + return false; + } + var referencedItem = referencedItems [0]; + if (!IsUpToDate (referencedItem)) { + info.IsUpToDate = false; + Log.LogMessage (MessageImportance.Low, "The assembly {0} is not up-to-date with regards to the reference {1}", assemblyPath, ar.Name); + return false; + } + } + + Log.LogMessage (MessageImportance.Low, $"The AOT-compiled code for {assemblyPath} is up-to-date."); + info.IsUpToDate = true; + return true; + } + + bool IsUpToDate (FileInfo input, FileInfo output) + { + if (!output.Exists) + return false; + return input.LastWriteTimeUtc <= output.LastWriteTimeUtc; + } + public override bool Execute () { var inputs = new List (Assemblies.Length); for (var i = 0; i < Assemblies.Length; i++) { - inputs.Add (Path.GetFullPath (Assemblies [i].ItemSpec)); + var input = Path.GetFullPath (Assemblies [i].ItemSpec); + inputs.Add (input); + Assemblies [i].SetMetadata ("Input", input); } // All the assemblies to AOT must be in the same directory @@ -64,24 +166,31 @@ public override bool Execute () InputDirectory = assemblyDirectories [0]; } + // Figure out which assemblies need to be aot'ed, and which are up-to-date. + var assembliesToAOT = Assemblies.Where (asm => !IsUpToDate (asm)).ToArray (); + if (assembliesToAOT.Length == 0) { + Log.LogMessage (MessageImportance.Low, $"All the AOT-compiled code is up-to-date."); + return !Log.HasLoggedErrors; + } + Directory.CreateDirectory (OutputDirectory); var aotAssemblyFiles = new List (); - var processes = new Task [Assemblies.Length]; + var processes = new Task [assembliesToAOT.Length]; var environment = new Dictionary { { "MONO_PATH", Path.GetFullPath (InputDirectory) }, }; var globalAotArguments = AotArguments?.Select (v => v.ItemSpec).ToList (); - for (var i = 0; i < Assemblies.Length; i++) { - var asm = Assemblies [i]; - var input = inputs [i]; - var arch = Assemblies [i].GetMetadata ("Arch"); - var aotArguments = Assemblies [i].GetMetadata ("Arguments"); - var processArguments = Assemblies [i].GetMetadata ("ProcessArguments"); - var aotData = Assemblies [i].GetMetadata ("AOTData"); - var aotAssembly = Assemblies [i].GetMetadata ("AOTAssembly"); + for (var i = 0; i < assembliesToAOT.Length; i++) { + var asm = assembliesToAOT [i]; + var input = asm.GetMetadata ("Input"); + var arch = asm.GetMetadata ("Arch"); + var aotArguments = asm.GetMetadata ("Arguments"); + var processArguments = asm.GetMetadata ("ProcessArguments"); + var aotData = asm.GetMetadata ("AOTData"); + var aotAssembly = asm.GetMetadata ("AOTAssembly"); var aotAssemblyItem = new TaskItem (aotAssembly); aotAssemblyItem.SetMetadata ("Arguments", "-Xlinker -rpath -Xlinker @executable_path/ -Qunused-arguments -x assembler -D DEBUG"); @@ -106,7 +215,7 @@ public override bool Execute () processes [i] = ExecuteAsync (AOTCompilerPath, arguments, environment: environment, sdkDevPath: SdkDevPath, showErrorIfFailure: false /* we show our own error below */) .ContinueWith ((v) => { if (v.Result.ExitCode != 0) - Log.LogError ("Failed to AOT compile {0}, the AOT compiler exited with code {1}", Path.GetFileName (input), v.Result.ExitCode); + Log.LogError (MSBStrings.E7118 /* Failed to AOT compile {0}, the AOT compiler exited with code {1} */, Path.GetFileName (input), v.Result.ExitCode); return System.Threading.Tasks.Task.FromResult (v.Result); }).Unwrap (); @@ -118,8 +227,8 @@ public override bool Execute () // For Windows support it's necessary to have the files we're going to create as an Output parameter, so that the files are // created on the windows side too, which makes the Inputs/Outputs logic work properly when working from Windows. - var objectFiles = Assemblies.Select (v => v.GetMetadata ("ObjectFile")).Where (v => !string.IsNullOrEmpty (v)); - var llvmFiles = Assemblies.Select (v => v.GetMetadata ("LLVMFile")).Where (v => !string.IsNullOrEmpty (v)); + var objectFiles = assembliesToAOT.Select (v => v.GetMetadata ("ObjectFile")).Where (v => !string.IsNullOrEmpty (v)); + var llvmFiles = assembliesToAOT.Select (v => v.GetMetadata ("LLVMFile")).Where (v => !string.IsNullOrEmpty (v)); FileWrites = objectFiles.Union (llvmFiles).Select (v => new TaskItem (v)).ToArray (); return !Log.HasLoggedErrors; diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/AppDelegate.cs b/tests/dotnet/RebuildTestAppWithLibraryReference/AppDelegate.cs new file mode 100644 index 00000000000..9c40ddb7276 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/AppDelegate.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +using Foundation; + +namespace MySimpleApp { + public class Program { + static int Main (string [] args) + { + GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + + Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD")); + Console.WriteLine (Library.Class.SomeFunction ()); + + return args.Length; + } + } +} diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Library.cs b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Library.cs new file mode 100644 index 00000000000..7f457d7e7ad --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Library.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Library { + public class Class { + public static string SomeFunction () + { +#if FIRSTBUILD + return "FIRSTBUILD"; +#elif SECONDBUILD + return "SECONDBUILD"; +#endif + } + } +} diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Library.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Library.csproj new file mode 100644 index 00000000000..6390277d5f5 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Library.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion) + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/MacCatalyst/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Makefile new file mode 100644 index 00000000000..a97c2cbb3d5 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/Makefile @@ -0,0 +1,2 @@ +TOP=../../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Library.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Library.csproj new file mode 100644 index 00000000000..6390277d5f5 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Library.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion) + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/iOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Library.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Library.csproj new file mode 100644 index 00000000000..6390277d5f5 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Library.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion) + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/macOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.csproj new file mode 100644 index 00000000000..b1a43cfef9e --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.csproj @@ -0,0 +1,12 @@ + + + + Library + $(DefineConstants);FIRSTBUILD + $(DefineConstants);SECONDBUILD + + + + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.mk b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.mk new file mode 100644 index 00000000000..3b1ea8d6cf3 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/shared.mk @@ -0,0 +1,2 @@ +TOP=../../../../.. +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Library.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Library.csproj new file mode 100644 index 00000000000..6390277d5f5 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Library.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion) + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Library/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/RebuildTestAppWithLibraryReference.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/RebuildTestAppWithLibraryReference.csproj new file mode 100644 index 00000000000..6b0e2c77318 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/MacCatalyst/RebuildTestAppWithLibraryReference.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/Makefile new file mode 100644 index 00000000000..6affa45ff12 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/Makefile @@ -0,0 +1,2 @@ +TOP=../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/RebuildTestAppWithLibraryReference.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/RebuildTestAppWithLibraryReference.csproj new file mode 100644 index 00000000000..86d408734aa --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/iOS/RebuildTestAppWithLibraryReference.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/RebuildTestAppWithLibraryReference.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/RebuildTestAppWithLibraryReference.csproj new file mode 100644 index 00000000000..a77287b9ba0 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/macOS/RebuildTestAppWithLibraryReference.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/shared.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/shared.csproj new file mode 100644 index 00000000000..532458ef45a --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/shared.csproj @@ -0,0 +1,19 @@ + + + + Exe + + RebuildTestAppWithLibraryReference + com.xamarin.rebuildtestappwithlibraryreference + + true + true + + + + + + + + + diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/shared.mk b/tests/dotnet/RebuildTestAppWithLibraryReference/shared.mk new file mode 100644 index 00000000000..f555cad4e80 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/shared.mk @@ -0,0 +1,2 @@ +TOP=../../../.. +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/Makefile b/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/Makefile new file mode 100644 index 00000000000..110d078f457 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/RebuildTestAppWithLibraryReference.csproj b/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/RebuildTestAppWithLibraryReference.csproj new file mode 100644 index 00000000000..bd487ddcd88 --- /dev/null +++ b/tests/dotnet/RebuildTestAppWithLibraryReference/tvOS/RebuildTestAppWithLibraryReference.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/UnitTests/RebuildTest.cs b/tests/dotnet/UnitTests/RebuildTest.cs new file mode 100644 index 00000000000..3fa72a7f2f6 --- /dev/null +++ b/tests/dotnet/UnitTests/RebuildTest.cs @@ -0,0 +1,39 @@ +namespace Xamarin.Tests { + [TestFixture] + public class RebuildTest : TestBaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + public void ReAotTest (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "RebuildTestAppWithLibraryReference"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + Clean (project_path); + + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["BuildCounter"] = "First"; + + DotNet.AssertBuild (project_path, properties); + + var appExecutable = Path.Combine (appPath, "Contents", "MacOS", Path.GetFileNameWithoutExtension (project_path)); + + Assert.That (appExecutable, Does.Exist, "There is an executable"); + if (CanExecute (platform, runtimeIdentifiers)) { + var output = ExecuteWithMagicWordAndAssert (appExecutable); + Assert.That (output, Does.Contain ("FIRSTBUILD"), "First build"); + } + + // Build again, changing something + properties ["BuildCounter"] = "Second"; + DotNet.AssertBuild (project_path, properties); + Assert.That (appExecutable, Does.Exist, "There is an executable"); + if (CanExecute (platform, runtimeIdentifiers)) { + var output = ExecuteWithMagicWordAndAssert (appExecutable); + Assert.That (output, Does.Contain ("SECONDBUILD"), "Second build"); + } + } + } +} + diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs index 6c7346be334..ff589f6a57f 100644 --- a/tests/dotnet/UnitTests/TestBaseClass.cs +++ b/tests/dotnet/UnitTests/TestBaseClass.cs @@ -308,19 +308,20 @@ protected string GenerateProject (ApplePlatform platform, string name, string ru return csproj; } - protected void ExecuteWithMagicWordAndAssert (ApplePlatform platform, string runtimeIdentifiers, string executable) + protected string ExecuteWithMagicWordAndAssert (ApplePlatform platform, string runtimeIdentifiers, string executable) { if (!CanExecute (platform, runtimeIdentifiers)) - return; + return string.Empty; - ExecuteWithMagicWordAndAssert (executable); + return ExecuteWithMagicWordAndAssert (executable); } - protected void ExecuteWithMagicWordAndAssert (string executable) + protected string ExecuteWithMagicWordAndAssert (string executable) { var rv = Execute (executable, out var output, out string magicWord); Assert.That (output.ToString (), Does.Contain (magicWord), "Contains magic word"); Assert.AreEqual (0, rv.ExitCode, "ExitCode"); + return output.ToString (); } protected Execution Execute (string executable, out StringBuilder output, out string magicWord)