Skip to content

Commit

Permalink
WIP for xamarin#3755.
Browse files Browse the repository at this point in the history
  • Loading branch information
rolfbjarne committed May 31, 2019
1 parent 8e57754 commit 867f281
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 98 deletions.
2 changes: 1 addition & 1 deletion Make.config
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ SYSTEM_MSBUILD=unset MSBuildExtensionsPath && $(MONO_PREFIX)/bin/msbuild
SYSTEM_RESGEN=$(MONO_PREFIX)/bin/resgen

XIBUILD_EXE_PATH=$(abspath $(TOP)/tools/xibuild/bin/Debug/xibuild.exe)
SYSTEM_XIBUILD=$(SYSTEM_MONO) $(XIBUILD_EXE_PATH) $(XIBUILD_VERBOSITY)
SYSTEM_XIBUILD=$(SYSTEM_MONO) --debug $(XIBUILD_EXE_PATH) $(XIBUILD_VERBOSITY)

PKG_CONFIG=$(MONO_PREFIX)/bin/pkg-config

Expand Down
85 changes: 64 additions & 21 deletions msbuild/Xamarin.MacDev.Tasks.Core/Tasks/DittoTaskBase.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

using Xamarin.Bundler;

namespace Xamarin.MacDev.Tasks
{
public abstract class DittoTaskBase : ToolTask
public abstract class DittoTaskBase : Task
{
#region Inputs

Expand All @@ -19,38 +23,77 @@ public abstract class DittoTaskBase : ToolTask
[Output]
public ITaskItem Destination { get; set; }

#endregion
[Output]
public ITaskItem[] CopiedFiles { get; set; }

protected override string ToolName {
get { return "ditto"; }
}
public ITaskItem ToolExe { get; set; }
public ITaskItem ToolPath { get; set; }

#endregion

protected override string GenerateFullPathToTool ()
public override bool Execute ()
{
if (!string.IsNullOrEmpty (ToolPath))
return Path.Combine (ToolPath, ToolExe);
var src = Path.GetFullPath (Source.ItemSpec);
var dst = Path.GetFullPath (Destination.ItemSpec);

var path = Path.Combine ("/usr/bin", ToolExe);
if (File.Exists (src)) {
Log.LogMessage ("Copying file from {0} to {1}: {2}", src, dst, Directory.Exists (dst));
if (Directory.Exists (dst))
dst = Path.Combine (dst, Path.GetFileName (src));
Log.LogMessage ("Copying file from {0} to {1}", src, dst);

return File.Exists (path) ? path : ToolExe;
}
if (!FileCopier.IsUptodate (src, dst, check_stamp: false)) {
files_copied.Add (dst);
Log.LogMessage ("Copied {0} to {1}", src, dst);
} else {
Log.LogMessage ("Target '{0}' is up-to-date", dst);
}

protected override string GenerateCommandLineCommands ()
{
var args = new CommandLineArgumentBuilder ();
} else if (Directory.Exists (src)) {
dst = Path.GetDirectoryName (dst);

args.Add ("-rsrc");
Log.LogMessage ("Copying directory from {0} to {1}", src, dst);

args.AddQuoted (Source.ItemSpec);
args.AddQuoted (Destination.ItemSpec);
FileCopier.UpdateDirectory (src, dst, CopyFileCallback);
} else {
Log.LogError ("Could not find the source location {0}", src);
}

return args.ToString ();
var copiedFiles = new List<ITaskItem> ();
foreach (var file in files_copied) {
copiedFiles.Add (new TaskItem (file));
Log.LogMessage ($"Copied: {file}");
}

return !Log.HasLoggedErrors;
}

protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
List<string> files_copied = new List<string> ();

CopyFileResult CopyFileCallback (CopyFileWhat what, CopyFileStep stage, IntPtr state, string source, string target, IntPtr ctx)
{
// TODO: do proper parsing of error messages and such
Log.LogMessage (messageImportance, "{0}", singleLine);
//Console.WriteLine ("CopyFileCallback ({0}, {1}, 0x{2}, {3}, {4}, 0x{5})", what, stage, state.ToString ("x"), source, target, ctx.ToString ("x"));
switch (what) {
case CopyFileWhat.File:
if (!FileCopier.IsUptodate (source, target, check_stamp: false)) {
if (stage == CopyFileStep.Start)
Log.LogMessage ("Copying {0} to {1}", source, target);
return CopyFileResult.Continue;
} else {
if (stage == CopyFileStep.Start)
Log.LogMessage ("Target '{0}' is up-to-date", target);
return CopyFileResult.Skip;
}
case CopyFileWhat.Error:
Log.LogError ("Could not copy the file '{0}' to '{1}': {2}", source, target, FileCopier.strerror (Marshal.GetLastWin32Error ()));
return CopyFileResult.Quit;
case CopyFileWhat.Dir:
case CopyFileWhat.DirCleanup:
case CopyFileWhat.CopyData:
case CopyFileWhat.CopyXattr:
default:
return CopyFileResult.Continue;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ void AssertProperlyCodesigned (bool expected)
}
}

void AssertNoModifiedFiles (DateTime timestamp, string path = null)
{
var files = Directory.EnumerateFiles (path ?? AppBundlePath, "*.*", SearchOption.TopDirectoryOnly);
var failures = new List<string> ();
foreach (var file in files) {
var newTimestamp = GetLastModified (file);
if (newTimestamp <= timestamp)
continue;

failures.Add ($"{file} was modified (current timestamp: {newTimestamp}, expected <= {timestamp}");
}
Assert.IsEmpty (failures, "No modified files");
}

[Test]
public void RebuildNoChanges ()
{
Expand All @@ -69,50 +83,54 @@ public void RebuildNoChanges ()
var appexDsymDir = Path.GetFullPath (Path.Combine (AppBundlePath, "..", "MyActionExtension.appex.dSYM"));

var timestamps = Directory.EnumerateFiles (AppBundlePath, "*.*", SearchOption.TopDirectoryOnly).ToDictionary (file => file, file => GetLastModified (file));
Dictionary<string, DateTime> dsymTimestamps = null, appexDsymTimestamps = null;
//Dictionary<string, DateTime> dsymTimestamps = null, appexDsymTimestamps = null;

if (Platform != "iPhoneSimulator") {
dsymTimestamps = Directory.EnumerateFiles (dsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
appexDsymTimestamps = Directory.EnumerateFiles (appexDsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
}
//if (Platform != "iPhoneSimulator") {
// dsymTimestamps = Directory.EnumerateFiles (dsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
// appexDsymTimestamps = Directory.EnumerateFiles (appexDsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
//}

EnsureFilestampChange ();
var timestamp = DateTime.Now;

// Rebuild w/ no changes
BuildProject ("MyTabbedApplication", Platform, config, clean: false);

AssertProperlyCodesigned (expectedCodesignResults);
AssertNoModifiedFiles (timestamp);

var newTimestamps = Directory.EnumerateFiles (AppBundlePath, "*.*", SearchOption.TopDirectoryOnly).ToDictionary (file => file, file => GetLastModified (file));
//var newTimestamps = Directory.EnumerateFiles (AppBundlePath, "*.*", SearchOption.TopDirectoryOnly).ToDictionary (file => file, file => GetLastModified (file));

foreach (var file in timestamps.Keys) {
// The executable files will all be newer because they get touched during each Build, all other files should not change
if (Path.GetFileName (file) == "MyTabbedApplication" || Path.GetExtension (file) == ".dylib")
continue;
//foreach (var file in timestamps.Keys) {
// // The executable files will all be newer because they get touched during each Build, all other files should not change
// if (Path.GetFileName (file) == "MyTabbedApplication" || Path.GetExtension (file) == ".dylib")
// continue;

Assert.AreEqual (timestamps[file], newTimestamps[file], "App Bundle timestamp changed: " + file);
}
// Assert.AreEqual (timestamps[file], newTimestamps[file], "App Bundle timestamp changed: " + file);
//}

if (Platform != "iPhoneSimulator") {
var newDsymTimestamps = Directory.EnumerateFiles (dsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
var newAppexDsymTimestamps = Directory.EnumerateFiles (appexDsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));

foreach (var file in dsymTimestamps.Keys) {
// The Info.plist should be newer because it gets touched
if (Path.GetFileName (file) == "Info.plist") {
Assert.IsTrue (dsymTimestamps[file] < newDsymTimestamps[file], "App Bundle dSYMs Info.plist not touched: " + file);
} else {
Assert.AreEqual (dsymTimestamps[file], newDsymTimestamps[file], "App Bundle dSYMs changed: " + file);
}
}

// The appex dSYMs will all be newer because they currently get regenerated after each Build due to the fact that the entire
// *.appex gets cloned into the app bundle each time.
//
// Note: we could fix this by not using `ditto` and instead implementing this ourselves to only overwrite files if they've changed
// and then setting some [Output] params that specify whether or not we need to re-codesign and/or strip debug symbols.
foreach (var file in appexDsymTimestamps.Keys)
Assert.IsTrue (appexDsymTimestamps[file] < newAppexDsymTimestamps[file], "App Extension dSYMs should be newer: " + file);
AssertNoModifiedFiles (timestamp, dsymDir);
AssertNoModifiedFiles (timestamp, appexDsymDir);
//var newDsymTimestamps = Directory.EnumerateFiles (dsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));
//var newAppexDsymTimestamps = Directory.EnumerateFiles (appexDsymDir, "*.*", SearchOption.AllDirectories).ToDictionary (file => file, file => GetLastModified (file));

//foreach (var file in dsymTimestamps.Keys) {
// // The Info.plist should be newer because it gets touched
// if (Path.GetFileName (file) == "Info.plist") {
// Assert.IsTrue (dsymTimestamps[file] < newDsymTimestamps[file], "App Bundle dSYMs Info.plist not touched: " + file);
// } else {
// Assert.AreEqual (dsymTimestamps[file], newDsymTimestamps[file], "App Bundle dSYMs changed: " + file);
// }
//}

//// The appex dSYMs will all be newer because they currently get regenerated after each Build due to the fact that the entire
//// *.appex gets cloned into the app bundle each time.
////
//// Note: we could fix this by not using `ditto` and instead implementing this ourselves to only overwrite files if they've changed
//// and then setting some [Output] params that specify whether or not we need to re-codesign and/or strip debug symbols.
//foreach (var file in appexDsymTimestamps.Keys)
//Assert.IsTrue (appexDsymTimestamps[file] < newAppexDsymTimestamps[file], "App Extension dSYMs should be newer: " + file);
}
}

Expand Down Expand Up @@ -161,12 +179,13 @@ public void RebuildWatchAppNoChanges ()
AssertProperlyCodesigned (expectedCodesignResults);

EnsureFilestampChange ();

var timestamp = DateTime.Now;
// Rebuild w/ no changes
BuildProject ("MyWatch2Container", Platform, config, clean: false);

// make sure everything is still codesigned properly
AssertProperlyCodesigned (expectedCodesignResults);
AssertNoModifiedFiles (timestamp);
}

[Test]
Expand Down Expand Up @@ -198,7 +217,7 @@ public void CodesignAfterModifyingWatchApp2Test ()

// make sure that the main app bundle was codesigned due to the changes in the appex
// Note: this step requires msbuild instead of xbuild to work properly
//Assert.IsTrue (newTimestamp > timestamp, "The main app bundle does not seem to have been re-codesigned");
Assert.IsTrue (newTimestamp > timestamp, "The main app bundle does not seem to have been re-codesigned");
} finally {
// restore the original ActionViewController.cs code...
text = text.Replace ("{0} The Awakening...", "{0} awake with context");
Expand Down
93 changes: 50 additions & 43 deletions tools/common/FileCopier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,59 @@
using System.Linq;
using System.Runtime.InteropServices;

namespace Xamarin.Bundler {
public static class FileCopier
namespace Xamarin.Bundler
{
[Flags]
enum CopyFileFlags : uint
{
enum CopyFileFlags : uint {
ACL = 1 << 0,
Stat = 1 << 1,
Xattr = 1 << 2,
Data = 1 << 3,
Security = Stat | ACL,
Metadata = Security | Xattr,
All = Metadata | Data,

Recursive = 1 << 15,
NoFollow_Src = 1 << 18,
NoFollow_Dst = 1 << 19,
Unlink = 1 << 21,
Nofollow = NoFollow_Src | NoFollow_Dst,
Clone = 1 << 24,
}
ACL = 1 << 0,
Stat = 1 << 1,
Xattr = 1 << 2,
Data = 1 << 3,
Security = Stat | ACL,
Metadata = Security | Xattr,
All = Metadata | Data,

Recursive = 1 << 15,
NoFollow_Src = 1 << 18,
NoFollow_Dst = 1 << 19,
Unlink = 1 << 21,
Nofollow = NoFollow_Src | NoFollow_Dst,
Clone = 1 << 24,
}

enum CopyFileState : uint {
StatusCB = 6,
}
enum CopyFileState : uint
{
StatusCB = 6,
}

enum CopyFileStep {
Start = 1,
Finish = 2,
Err = 3,
Progress = 4,
}
public enum CopyFileStep
{
Start = 1,
Finish = 2,
Err = 3,
Progress = 4,
}

enum CopyFileResult {
Continue = 0,
Skip = 1,
Quit = 2,
}
public enum CopyFileResult
{
Continue = 0,
Skip = 1,
Quit = 2,
}

enum CopyFileWhat {
Error = 0,
File = 1,
Dir = 2,
DirCleanup = 3,
CopyData = 4,
CopyXattr = 5,
}
public enum CopyFileWhat
{
Error = 0,
File = 1,
Dir = 2,
DirCleanup = 3,
CopyData = 4,
CopyXattr = 5,
}

public static class FileCopier
{
[DllImport ("/usr/lib/libSystem.dylib")]
static extern IntPtr copyfile_state_alloc ();

Expand All @@ -59,7 +66,7 @@ enum CopyFileWhat {
[DllImport ("/usr/lib/libSystem.dylib")]
static extern int copyfile_state_set (IntPtr state, CopyFileState flag, IntPtr value);

delegate CopyFileResult CopyFileCallbackDelegate (CopyFileWhat what, CopyFileStep stage, IntPtr state, string src, string dst, IntPtr ctx);
public delegate CopyFileResult CopyFileCallbackDelegate (CopyFileWhat what, CopyFileStep stage, IntPtr state, string src, string dst, IntPtr ctx);

[DllImport ("/usr/lib/libSystem.dylib", SetLastError = true)]
static extern int copyfile (string @from, string @to, IntPtr state, CopyFileFlags flags);
Expand All @@ -75,7 +82,7 @@ enum CopyFileWhat {
public static Exception CreateError (int code, string message, params object[] args) => throw new Exception ($"{code} {string.Format (message, args)}");
#endif

public static void UpdateDirectory (string source, string target)
public static void UpdateDirectory (string source, string target, CopyFileCallbackDelegate callback = null)
{
if (!Directory.Exists (target))
Directory.CreateDirectory (target);
Expand All @@ -84,7 +91,7 @@ public static void UpdateDirectory (string source, string target)
// so we need to use native functions directly. Luckily OSX provides exactly what we need.
IntPtr state = copyfile_state_alloc ();
try {
CopyFileCallbackDelegate del = CopyFileCallback;
CopyFileCallbackDelegate del = callback ?? CopyFileCallback;
copyfile_state_set (state, CopyFileState.StatusCB, Marshal.GetFunctionPointerForDelegate (del));
int rv = copyfile (source, target, state, CopyFileFlags.Data | CopyFileFlags.Recursive | CopyFileFlags.Nofollow | CopyFileFlags.Clone);
if (rv != 0)
Expand Down

0 comments on commit 867f281

Please sign in to comment.