Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1719,4 +1719,34 @@
<value>Did not extract {0} because it would write outside the target directory.</value>
</data>

<data name="E7149" xml:space="preserve">
<value>Unable to calculate the assemblies report from '{0}'. Directory not found.</value>
<comment>{0}: path from where to calculate assemblies report</comment>
</data>

<data name="W7145" xml:space="preserve">
<value>Unable to retrieve information from '{0}'. The file may not be a valid PE file.</value>
<comment>{0}: path of the file that failed the VMID calculation</comment>
</data>

<data name="E7150" xml:space="preserve">
<value>Unable to calculate assemblies report from '{0}'. An unexpected error occurred.</value>
<comment>{0}: path from where to calculate assemblies report</comment>
</data>

<data name="E7146" xml:space="preserve">
<value>Unable to analyze file changes in '{0}'. Directory not found.</value>
<comment>{0}: path to use for file changes calculation</comment>
</data>

<data name="E7147" xml:space="preserve">
<value>Unable to analyze file changes in '{0}'. The report file '{1}' does not exist.</value>
<comment>{0}: path to use for file changes calculation
{1}: path of the assemblies report file</comment>
</data>

<data name="E7148" xml:space="preserve">
<value>Unable to analyze file changes in '{0}'. An unexpected error occurred.</value>
<comment>{0}: path to use for file changes calculation</comment>
</data>
</root>
127 changes: 127 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/AnalyzeFileChanges.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;

namespace Xamarin.MacDev.Tasks {
public class AnalyzeFileChanges : XamarinTask, ITaskCallback {
[Required]
public string WorkingDirectory { get; set; } = string.Empty;

[Required]
public ITaskItem? ReportFile { get; set; }

[Output]
public ITaskItem [] ChangedFiles { get; set; } = [];

public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => [];

//We need the ReportFile input to be copied to the Mac. Since it's the only ITaskItem input, it's safe to return true
public bool ShouldCopyToBuildServer (ITaskItem item) => true;

//In case it's a remote execution, we don't want empty output files to be copied since we need the real files to be copied
public bool ShouldCreateOutputFile (ITaskItem item) => false;

public override bool Execute ()
{
if (ShouldExecuteRemotely ()) {
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;
}

if (!Directory.Exists (WorkingDirectory)) {
Log.LogError (MSBStrings.E7146 /* Unable to analyze file changes in '{0}'. Directory not found. */, WorkingDirectory);

return false;
}

if (!File.Exists (ReportFile!.ItemSpec)) {
Log.LogError (MSBStrings.E7147 /* Unable to analyze file changes in '{0}'. The report file '{1}' does not exist. */, WorkingDirectory, ReportFile.ItemSpec);

return false;
}

try {
var changedFiles = new List<ITaskItem> ();
//Gets a dictionary of file names, lengths and MVIDs from the ReportFile
IDictionary<string, (long length, Guid mvid)> reportFileList = GetReportFileList ();
IEnumerable<string> files = Directory.GetFiles (WorkingDirectory, "*.dll", SearchOption.TopDirectoryOnly);

foreach (string file in files) {
//If there is a new assembly in the remote side not present in the report file, we register it for copying back
if (!reportFileList.TryGetValue (Path.GetFileName (file), out (long length, Guid mvid) localInfo)) {
changedFiles.Add (new TaskItem (file));
TryAddPdbFile (file, changedFiles);

continue;
}

var fileInfo = new FileInfo (file);

//If the file lengths differ, it means local and remote versions are different
if (fileInfo.Length != localInfo.length) {
changedFiles.Add (new TaskItem (file));
TryAddPdbFile (file, changedFiles);

continue;
}

using Stream stream = fileInfo.OpenRead ();
using var peReader = new PEReader (stream);
MetadataReader metadataReader = peReader.GetMetadataReader ();
Guid mvid = metadataReader.GetGuid (metadataReader.GetModuleDefinition ().Mvid);

//If the MVID from the report file (local MVID) is different than the calculated MVID of the file, it means local and remote versions are different
if (mvid != localInfo.mvid) {
changedFiles.Add (new TaskItem (file));
TryAddPdbFile (file, changedFiles);
}
}

ChangedFiles = [.. changedFiles];

return true;
} catch (Exception ex) {
Log.LogError (MSBStrings.E7148 /* Unable to analyze file changes in '{0}'. An unexpected error occurred. */, WorkingDirectory);
Log.LogErrorFromException (ex);

return false;
}
}

IDictionary<string, (long length, Guid mvid)> GetReportFileList ()
{
var reportFileList = new Dictionary<string, (long length, Guid mvid)> ();

//Expected format of the report file lines (defined in the CalculateAssembliesReport task): Foo.dll/23189/768C814C-05C3-4563-9B53-35FEF571968E
foreach (var line in File.ReadLines (ReportFile!.ItemSpec)) {
string [] lineParts = line.Split (['/'], StringSplitOptions.RemoveEmptyEntries);

// Skip lines that don't match the expected format
if (lineParts.Length == 3 && long.TryParse (lineParts [1], out long fileLength) && Guid.TryParse (lineParts [2], out Guid mvid)) {
// Adds file name, length and MVID to the dictionary
reportFileList.Add (lineParts [0], (fileLength, mvid));
}
}

return reportFileList;
}

bool TryAddPdbFile (string file, List<ITaskItem> changedFiles)
{
var pdbFile = Path.ChangeExtension (file, ".pdb");

if (!File.Exists (pdbFile)) {
return false;
}

changedFiles.Add (new TaskItem (pdbFile));

return true;
}
}
}
63 changes: 63 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/CalculateAssembliesReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using Microsoft.Build.Framework;
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;

namespace Xamarin.MacDev.Tasks {
public class CalculateAssembliesReport : XamarinTask {
[Required]
public string WorkingDirectory { get; set; } = string.Empty;

[Required]
public string TargetReportFile { get; set; } = string.Empty;

public override bool Execute ()
{
if (ShouldExecuteRemotely ()) {
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;
}

if (!Directory.Exists (WorkingDirectory)) {
Log.LogError (MSBStrings.E7149 /* Unable to calculate the assemblies report from '{0}'. Directory not found. */, WorkingDirectory);

return false;
}

try {
var reportEntriesBuilder = new StringBuilder ();
IEnumerable<string> files = Directory.GetFiles (WorkingDirectory, "*.dll", SearchOption.TopDirectoryOnly);

foreach (var file in files) {
try {
var fileInfo = new FileInfo (file);
using Stream stream = fileInfo.OpenRead ();
using var peReader = new PEReader (stream);
MetadataReader metadataReader = peReader.GetMetadataReader ();
Guid mvid = metadataReader.GetGuid (metadataReader.GetModuleDefinition ().Mvid);

//Appending the file name, length and mvid like: Foo.dll/23189/768C814C-05C3-4563-9B53-35FEF571968E
reportEntriesBuilder.AppendLine ($"{Path.GetFileName (file)}/{fileInfo.Length}/{mvid}");
} catch (Exception) {
Log.LogWarning (MSBStrings.W7145 /* Unable to retrieve information from '{0}'. The file may not be a valid PE file."\ */, Path.GetFileName (file));
continue;
}
}

//Creates or overwrites the report file
File.WriteAllText (TargetReportFile, reportEntriesBuilder.ToString ());

return true;
} catch (Exception ex) {
Log.LogError (MSBStrings.E7150 /* Unable to calculate assemblies report from '{0}'. An unexpected error occurred. */, WorkingDirectory);
Log.LogErrorFromException (ex);

return false;
}
}
}
}
6 changes: 4 additions & 2 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/Zip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#nullable enable

namespace Xamarin.MacDev.Tasks {
public class Zip : XamarinTask, ICancelableTask {
public class Zip : XamarinTask, ICancelableTask, ITaskCallback {
CancellationTokenSource? cancellationTokenSource;

#region Inputs
Expand Down Expand Up @@ -101,9 +101,11 @@ public void Cancel ()
}
}

//We don't want the inputs to be copied to the Mac since when zipping remotely, we are expecting the files to be already present in the Mac
public bool ShouldCopyToBuildServer (ITaskItem item) => false;

public bool ShouldCreateOutputFile (ITaskItem item) => true;
//We don't want empty output files to be created in Windows since we are already copying the real output file as part of the task execution
public bool ShouldCreateOutputFile (ITaskItem item) => false;

public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ Copyright (C) 2011-2013 Xamarin. All rights reserved.
<MakeDir Condition="'$(MtouchTargetsEnabled)' And '$(IsMacEnabled)' == 'true'" SessionId="$(BuildSessionId)" Directories="$(DeviceSpecificOutputPath)AppBundle" />
<!--Zip AppBundle-->
<Zip SessionId="$(BuildSessionId)" Condition="'$(MtouchTargetsEnabled)' And '$(IsMacEnabled)' == 'true'" ZipPath="$(ZipPath)" Recursive="true" Sources="$(DeviceSpecificOutputPath)$(_AppBundleName)$(AppBundleExtension)" OutputFile="$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName).zip" WorkingDirectory="$(DeviceSpecificOutputPath)AppBundle" />
<!--Copy Zip from Mac-->
<Message Text="$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName).zip" />
<CopyFileFromBuildServer Condition="'$(MtouchTargetsEnabled)' And '$(IsMacEnabled)' == 'true'" SessionId="$(BuildSessionId)" File="$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName).zip" />
<!--Unzip App Bundle on Windows-->
<Unzip Condition="Exists('$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName).zip')" ZipFilePath="$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName).zip" ExtractionPath="$(DeviceSpecificOutputPath)AppBundle\$(_AppBundleName)$(AppBundleExtension)" />
<!-- Delete Zip file -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@ Copyright (C) 2011-2013 Xamarin. All rights reserved.
***********************************************************************************************
-->
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="Xamarin.MacDev.Tasks.CalculateAssembliesReport" AssemblyFile="$(CoreiOSSdkDirectory)$(_TaskAssemblyFileName)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.AnalyzeFileChanges" AssemblyFile="$(CoreiOSSdkDirectory)$(_TaskAssemblyFileName)" />

<Import Project="$(MSBuildThisFileDirectory)Xamarin.Messaging.Build.targets" Condition="Exists('$(MSBuildThisFileDirectory)Xamarin.Messaging.Build.targets') And '$(MessagingBuildTargetsImported)' != 'true'" />
<Import Project="$(MSBuildThisFileDirectory)Xamarin.Messaging.Apple.targets" Condition="Exists('$(MSBuildThisFileDirectory)Xamarin.Messaging.Apple.targets') And '$(MessagingAppleTargetsImported)' != 'true'" />

<PropertyGroup>
<!-- Allows to delete the entire build directory for the app in the Mac, which means that all the SessionId based generated directories will be deleted. -->
<!-- Specially useful for CI builds where the user wants to clean previous builds -->
<RemoveAppDir Condition="'$(RemoveAppDir)' == ''">false</RemoveAppDir>
<!-- By default we don't want to add overhead in remote builds by keeping remote and local outputs in sync -->
<!-- This is only needed for builds that are intended to be debugged, so VS should set this property to true when debugging -->
<KeepLocalOutputUpToDate Condition="'$(KeepLocalOutputUpToDate)' == ''">false</KeepLocalOutputUpToDate>
<!-- By default the zip file that contains the modified files to update locally is meant to be cleaned -->
<CleanChangedOutputFilesZipFile Condition="'$(CleanChangedOutputFilesZipFile)' == ''">true</CleanChangedOutputFilesZipFile>
<LocalOutputReportFileName>OutputAssembliesReport.txt</LocalOutputReportFileName>
</PropertyGroup>

<!-- AfterClean belongs to Microsoft.Common.CurrentVersion.targets and it's the last target of the $(CleanDependsOn) -->
Expand All @@ -27,5 +36,38 @@ Copyright (C) 2011-2013 Xamarin. All rights reserved.
<RemoveDir SessionId="$(BuildSessionId)" Condition="'$(MtouchTargetsEnabled)' == 'true'" Directories="$(OutputPath);$(IntermediateOutputPath)" RemoveAppDir="$(RemoveAppDir)" ContinueOnError="true" />
</Target>

<!-- Target meant to run locally (Windows), as part of a remote build, to obtain information of the OutputPath for .dll files, generating a resulting report file with it -->
<!-- We don't want the remote build to fail in case this target fails, so we treat errors as warnings -->
<Target Name="GenerateLocalOutputReport" Condition="'$(OutputType)' == 'Exe' And '$(KeepLocalOutputUpToDate)' == 'true' And '$(IsRemoteBuild)' == 'true' And '$(IsMacEnabled)' == 'true'" BeforeTargets="BeforeDisconnect" Inputs="$(OutputPath)" Outputs="$(IntermediateOutputPath)$(LocalOutputReportFileName)">
<!-- This task will run locally since we don't pass the SessionId -->
<CalculateAssembliesReport WorkingDirectory="$(OutputPath)" TargetReportFile="$(IntermediateOutputPath)$(LocalOutputReportFileName)" ContinueOnError="WarnAndContinue"/>
</Target>

<!-- Target meant to run part locally (Windows) and part remotely (Mac), as part of a remote build, to analyze which files from the remote OutputPath have changed, compared to the local OutputPath, and to copy those files back to the OutputPath in Windows -->
<!-- We don't want the remote build to fail in case this target fails, so we treat errros as warnings -->
<Target Name="CopyChangedOutputFilesFromMac" Condition="'$(OutputType)' == 'Exe' And '$(KeepLocalOutputUpToDate)' == 'true' And '$(IsRemoteBuild)' == 'true' And '$(IsMacEnabled)' == 'true'" AfterTargets="GenerateLocalOutputReport">
<PropertyGroup>
<!-- The remote output path needs to be the resulting .app path, which will contains all the final files -->
<AppBundleDir>$(DeviceSpecificOutputPath)$(_AppBundleName)$(AppBundleExtension)\</AppBundleDir>
<ChangedOutputFilesZipFile>$(IntermediateOutputPath)ChangedOutputFiles.zip</ChangedOutputFilesZipFile>
</PropertyGroup>

<!-- Task that will run remotely and analyze the files that are different between local and remote output paths -->
<!-- The ReportFile will be copied to the Mac because it's an ITaskItem input -->
<AnalyzeFileChanges SessionId="$(BuildSessionId)" WorkingDirectory="$(AppBundleDir)" ReportFile="$(IntermediateOutputPath)$(LocalOutputReportFileName)" ContinueOnError="WarnAndContinue">
<Output TaskParameter="ChangedFiles" ItemName="FilesToCopyBack"/>
</AnalyzeFileChanges>

<!-- Task that will run remotely and zip the changed files (FilesToCopyBack), resulting of the AnalyzeFileChanges run -->
<!-- Because the task runs remotely, it will copy the zipped file back to Windows, to the same relative location that the OutputFile is on the remote side -->
<Zip Condition="'@(FilesToCopyBack)' != ''" SessionId="$(BuildSessionId)" WorkingDirectory="$(AppBundleDir)" Sources="@(FilesToCopyBack)" OutputFile="$(ChangedOutputFilesZipFile)" ContinueOnError="WarnAndContinue"/>

<!-- Task that will run locally and unzip the changed files copied from the Mac to the local output path -->
<Unzip Condition="Exists('$(ChangedOutputFilesZipFile)')" ZipFilePath="$(ChangedOutputFilesZipFile)" ExtractionPath="$(OutputPath)" ContinueOnError="WarnAndContinue"/>

<!-- Task that will run remotely and delete the zip file in both Mac and Windows -->
<Delete SessionId="$(BuildSessionId)" Condition="'$(CleanChangedOutputFilesZipFile)' == 'true' And Exists('$(ChangedOutputFilesZipFile)')" Files="$(ChangedOutputFilesZipFile)" ContinueOnError="WarnAndContinue"/>
</Target>

<Import Project="$(MSBuildThisFileDirectory)Xamarin.iOS.HotRestart.targets" Condition="Exists('$(MSBuildThisFileDirectory)Xamarin.iOS.HotRestart.targets')" />
</Project>
Loading