xamarin-macios/tools/common/cache.cs

528 строки
15 KiB
C#
Исходник Обычный вид История

2016-04-21 15:57:02 +03:00
// Copyright 2012 Xamarin Inc. All rights reserved.
//#define DEBUG_COMPARE
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
2016-04-21 15:57:02 +03:00
using System.Text;
using Xamarin.Utils;
2016-04-21 15:57:02 +03:00
using Xamarin.Bundler;
public class Cache {
2016-04-21 15:57:02 +03:00
#if MMP
const string NAME = "mmp";
#elif MTOUCH
const string NAME = "mtouch";
#elif BUNDLER
const string NAME = "dotnet-linker";
2016-04-21 15:57:02 +03:00
#else
#error Wrong defines
#endif
string cache_dir;
bool temporary_cache;
string[] arguments;
public Cache (string[] arguments)
{
this.arguments = arguments;
}
2016-04-21 15:57:02 +03:00
public bool IsCacheTemporary {
2016-04-21 15:57:02 +03:00
get { return temporary_cache; }
}
// see --cache=DIR
public string Location {
2016-04-21 15:57:02 +03:00
get {
if (cache_dir == null) {
do {
cache_dir = Path.Combine (Path.GetTempPath (), NAME + ".cache", Path.GetRandomFileName ());
if (File.Exists (cache_dir) || Directory.Exists (cache_dir))
continue;
Directory.CreateDirectory (cache_dir);
break;
} while (true);
cache_dir = Target.GetRealPath (cache_dir);
temporary_cache = true;
if (!Directory.Exists (cache_dir))
Directory.CreateDirectory (cache_dir);
#if DEBUG
Console.WriteLine ("Cache defaults to {0}", cache_dir);
#endif
}
return cache_dir;
}
set {
cache_dir = value;
if (!Directory.Exists (cache_dir))
Directory.CreateDirectory (cache_dir);
cache_dir = Target.GetRealPath (Path.GetFullPath (cache_dir));
}
}
public void Clean ()
2016-04-21 15:57:02 +03:00
{
#if DEBUG
Console.WriteLine ("Cache.Clean: {0}" , Location);
#endif
Directory.Delete (Location, true);
Directory.CreateDirectory (Location);
}
public static bool CompareDirectories (string a, string b, bool ignore_cache = false)
{
if (Driver.Force && !ignore_cache) {
Driver.Log (6, "Directories {0} and {1} are considered different because -f was passed to " + NAME + ".", a, b);
return false;
}
var diff = new StringBuilder ();
Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. (#7177) * Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. mono changed how quotes should be escaped when passed to System.Diagnostic.Process, so we need to change accordingly. The main difference is that single quotes don't have to be escaped anymore. This solves problems like this: System.ComponentModel.Win32Exception : ApplicationName='nuget', CommandLine='restore '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable/CellCustomTable.sln' -Verbosity detailed -SolutionDir '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable'', CurrentDirectory='/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories', Native error= Cannot find the specified file at System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) [0x0029f] in /Users/builder/jenkins/workspace/build-package-osx-mono/2019-08/external/bockbuild/builds/mono-x64/mcs/class/System/System.Diagnostics/Process.cs:778 ref: https://github.com/mono/mono/pull/15047 * Rework process arguments to pass arrays/lists around instead of quoted strings. And then only convert to a string at the very end when we create the Process instance. In the future there will be a ProcessStartInfo.ArgumentList property we can use to give the original array/list of arguments directly to the BCL so that we can avoid quoting at all. These changes gets us almost all the way there already (except that the ArgumentList property isn't available quite yet). We also have to bump to target framework version v4.7.2 from v4.5 in several places because of 'Array.Empty<T> ()' which is now used in more places. * Parse linker flags from LinkWith attributes. * [sampletester] Bump to v4.7.2 for Array.Empty<T> (). * Fix typo. * Rename GetVerbosity -> AddVerbosity. * Remove unnecessary string interpolation. * Remove unused variable. * [mtouch] Simplify code a bit. * Use implicitly typed arrays.
2019-10-14 17:18:46 +03:00
if (Driver.RunCommand ("diff", new [] { "-ur", a, b, }, output: diff, suppressPrintOnErrors: true) != 0) {
Driver.Log (1, "Directories {0} and {1} are considered different because diff said so:\n{2}", a, b, diff);
return false;
}
return true;
}
2016-04-21 15:57:02 +03:00
public static bool CompareFiles (string a, string b, bool ignore_cache = false)
{
if (Driver.Force && !ignore_cache) {
Driver.Log (6, "Files {0} and {1} are considered different because -f was passed to " + NAME + ".", a, b);
2016-04-21 15:57:02 +03:00
return false;
}
if (!File.Exists (b)) {
Driver.Log (6, "Files {0} and {1} are considered different because the latter doesn't exist.", a, b);
2016-04-21 15:57:02 +03:00
return false;
}
using (var astream = new FileStream (a, FileMode.Open, FileAccess.Read, FileShare.Read)) {
using (var bstream = new FileStream (b, FileMode.Open, FileAccess.Read, FileShare.Read)) {
2016-04-21 15:57:02 +03:00
bool rv;
Driver.Log (6, "Comparing files {0} and {1}...", a, b);
2016-04-21 15:57:02 +03:00
rv = CompareStreams (astream, bstream, ignore_cache);
Driver.Log (6, " > {0}", rv ? "Identical" : "Different");
2016-04-21 15:57:02 +03:00
return rv;
}
}
}
public static bool CompareAssemblies (string a, string b, bool ignore_cache = false, bool compare_guids = false)
{
if (Driver.Force && !ignore_cache) {
Driver.Log (6, "Assemblies {0} and {1} are considered different because -f was passed to " + NAME + ".", a, b);
2016-04-21 15:57:02 +03:00
return false;
}
if (!File.Exists (b)) {
Driver.Log (6, "Assemblies {0} and {1} are considered different because the latter doesn't exist.", a, b);
2016-04-21 15:57:02 +03:00
return false;
}
using (var astream = new AssemblyReader (a) { CompareGUIDs = compare_guids }) {
using (var bstream = new AssemblyReader (b) { CompareGUIDs = compare_guids }) {
bool rv;
Driver.Log (6, "Comparing assemblies {0} and {1}...", a, b);
2016-04-21 15:57:02 +03:00
rv = CompareStreams (astream, bstream, ignore_cache);
Driver.Log (6, " > {0}", rv ? "Identical" : "Different");
2016-04-21 15:57:02 +03:00
return rv;
}
}
}
public unsafe static bool CompareStreams (Stream astream, Stream bstream, bool ignore_cache = false)
{
if (Driver.Force && !ignore_cache) {
Driver.Log (6, " > streams are considered different because -f was passed to " + NAME + ".");
2016-04-21 15:57:02 +03:00
return false;
}
if (astream.Length != bstream.Length) {
Driver.Log (6, " > streams are considered different because their lengths do not match.");
2016-04-21 15:57:02 +03:00
return false;
}
var ab = new byte[2048];
var bb = new byte[2048];
do {
int ar = astream.Read (ab, 0, ab.Length);
int br = bstream.Read (bb, 0, bb.Length);
if (ar != br) {
Driver.Log (6, " > streams are considered different because their lengths do not match.");
2016-04-21 15:57:02 +03:00
return false;
}
if (ar == 0)
return true;
fixed (byte *aptr = ab, bptr = bb) {
long *l1 = (long *) aptr;
long *l2 = (long *) bptr;
int len = ar;
// Compare one long at a time.
for (int i = 0; i < len / 8; i++) {
if (l1 [i] != l2 [i]) {
Driver.Log (6, " > streams differ at index {0}-{1}", i, i + 8);
2016-04-21 15:57:02 +03:00
return false;
}
}
// Compare any remaining bytes.
int mod = len % 8;
if (mod > 0) {
for (int i = len - mod; i < len; i++) {
if (ab [i] != bb [i]) {
Driver.Log (6, " > streams differ at byte index {0}", i);
2016-04-21 15:57:02 +03:00
return false;
}
}
}
}
} while (true);
}
string GetArgumentsForCacheData ()
2016-04-21 15:57:02 +03:00
{
var sb = new StringBuilder ();
var args = new List<string> (arguments);
2016-04-21 15:57:02 +03:00
sb.Append ("# Version: ").Append (Constants.Version).Append ('.').Append (Constants.Revision).AppendLine ();
sb.Append (Driver.GetFullPath ()).AppendLine (" \\");
[mtouch] Fix cache.cs wrt response files. Fix #7514 (#7650) TL&DR * re-apply the fix to cache.cs from https://github.com/xamarin/xamarin-macios/pull/7544 * which was reverted in https://github.com/xamarin/xamarin-macios/pull/7589 * since it regressed mscorlib/sim testing in xharness (for other reasons) * Final part to fix https://github.com/xamarin/xamarin-macios/issues/7514 This was the ~night~ day before christmas... amd a tough nut to crack! Thanksfully we had a good test case (inside #7514) and then xharness regressed one test in consistent, reproducible manner. xharness builds mscorlib tests twice (32 and 64 bits) even if it's a fat application (could be reused). That should not be a huge problem since the 2nd build should be identical and the cache should be (re)used. An earlier attempt fixed this (comparison was true for the wrong reasons [1]) but the fix did not end up with the same arguments !?! and was reverted. This is the diff between the first and second builds: ```diff --- /Users/poupou/a.txt 2019-12-23 09:55:01.000000000 -0500 +++ /Users/poupou/b.txt 2019-12-23 09:55:01.000000000 -0500 @@ -182,5 +182,5 @@ -r=/Users/poupou/git/xamarin/xamarin-macios/builds/downloads/ios-release-Darwin-8f396bbb408b5758fccb8602030b9fa5293ce718/ios-bcl/monotouch/tests/Xunit.NetCore.Extensions.dll \ ' --target-framework=Xamarin.iOS,v1.0' \ --root-assembly=/Users/poupou/git/xamarin/xamarin-macios/tests/xharness/tmp-test-dir/mscorlib/bin/mscorlib/iPhoneSimulator/Debug-unified/com.xamarin.bcltests.mscorlib.exe \ - ' -v -v -v -v' \ + ' -v -v' \ @/Users/poupou/git/xamarin/xamarin-macios/tests/xharness/tmp-test-dir/mscorlib/obj/iPhoneSimulator/Debug/response-file.rsp \ ``` Since they are not identical the cache is invalidated (which is normal, cache-wise) and produce an output app that is incorrect (and crash 32bits). Now there is code to ignore verbosity options (both `-v` and -q`) since they will not affect what `mtouch` generates. However this was broken because mtouch's response-file parser is quite basic and stricter the the specification spec: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/response-file-compiler-option issue: https://github.com/xamarin/xamarin-macios/issues/7644 That failed in two different ways 1. note the extra space before the first `-v` in the diff (before the `'` quote). That skipped the line. 2. there are multiple `-v` in the same line, again that make the filtering skip the line. *Unknowns* It's too close to xmas/vacation so I might not find the reasons/issues for the following, unanswered questions... 1. Why is the re-build app bundle failing at runtime when p/invoking ? Something is not regenerated (symbol maps?) ? 2. Why xharness 2nd build has more verbosity than the first one (likely harmless) ? [1] the original cache.cs issue (prequel) issue w/test case: https://github.com/xamarin/xamarin-macios/issues/7514 first attempt: https://github.com/xamarin/xamarin-macios/pull/7544 While incorrect the first attempt to fix `cache.cs` was a logical, if not entirely complete, fix. Without it this is what we _currently_ cache: ``` /Users/poupou/git/xamarin/xamarin-macios/_ios-build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/lib/mtouch/mtouch.exe \ ``` and that does not include any of mtouch's arguments, that can change between executions and (should) invalidate the cache. In this case it means the cache is used (no difference) but this does **not** parse the content of the **response file** which is obviously wrong (and we do have code to process it). On the original issue's test case this is what makes the difference between using the same *old* nuget assembly after an update (and fail) by itself and also because the updated framework was not copied (due to the 2nd part of the bug report wrt `copyfile`). OTOH re-using (incorrectly) the cache is what makes xharness's mscorlib unit tests works right now :(
2020-01-02 23:01:55 +03:00
CollectArgumentsForCache (args, 0, sb);
return sb.ToString ();
}
void CollectArgumentsForCache (IList<string> args, int firstArgument, StringBuilder sb)
{
for (int i = firstArgument; i < args.Count; i++) {
var arg = args [i];
switch (arg) {
2016-04-21 15:57:02 +03:00
// Remove arguments that don't affect the cache status.
case "":
2016-04-21 15:57:02 +03:00
case "/v":
case "-v":
case "--v":
case "/f":
case "-f":
case "--f":
case "/time":
case "-time":
case "--time":
break;
default:
if (arg [0] == '@')
CollectArgumentsForCache (File.ReadAllLines (arg.Substring (1)), 0, sb);
sb.Append ('\t').Append (StringUtils.Quote (arg)).AppendLine (" \\");
2016-04-21 15:57:02 +03:00
break;
}
}
}
public bool IsCacheValid ()
2016-04-21 15:57:02 +03:00
{
var name = "arguments";
var pcache = Path.Combine (Location, name);
if (!File.Exists (pcache)) {
Driver.Log (3, "A full rebuild will be performed because the cache is either incomplete or entirely missing.");
return false;
} else if (GetArgumentsForCacheData () != File.ReadAllText (pcache)) {
Driver.Log (3, "A full rebuild will be performed because the arguments to " + NAME + " has changed with regards to the cached data.");
return false;
}
// Check if mtouch/mmp has been modified.
var executable = System.Reflection.Assembly.GetExecutingAssembly ().Location;
if (!Application.IsUptodate (executable, pcache)) {
Driver.Log (3, "A full rebuild will be performed because " + NAME + " has been modified.");
2016-04-21 15:57:02 +03:00
return false;
}
return true;
}
public bool VerifyCache ()
2016-04-21 15:57:02 +03:00
{
if (!IsCacheValid ()) {
Clean ();
return false;
}
return true;
}
public void ValidateCache ()
2016-04-21 15:57:02 +03:00
{
var name = "arguments";
var pcache = Path.Combine (Location, name);
File.WriteAllText (pcache, GetArgumentsForCacheData ());
}
// A stream that reads an assembly and skips the header and the GUID table.
class AssemblyReader : Stream {
string filename;
FileStream stream;
long guid_table_start;
long guid_table_length;
public bool CompareGUIDs;
public AssemblyReader (string filename)
{
this.filename = filename;
// Need to figure out where the #GUID table is so we can ignore it.
FindGUIDTable ();
stream = File.OpenRead (filename);
}
public override int Read (byte[] buffer, int offset, int count)
{
// read the header, always the same 136 bytes, followed by a 4 bytes timestamp (which we must ignore)
// the rest (except the #GUID table) is safe to compare.
if (stream.Position < 136) {
// read the first 136 bytes
int read = stream.Read (buffer, offset, 136 - (int) stream.Position);
if (stream.Position == 136) {
// skip the timestamp
stream.Position += 4;
// this prints the timestamp:
// byte[] buf = new byte[4];
// stream.Read (buf, 0, 4);
// int t2 = (buf [3] << 24) + (buf [2] << 16) + (buf [1] << 8) + buf [0];
// var d = new DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// var d2 = d.AddSeconds (t2);
// Console.WriteLine ("TS of {1}: {0}", d2, filename);
}
return read; // don't bother reading more, this makes the implementation easier.
}
if (CompareGUIDs)
return stream.Read (buffer, offset, count);
if (stream.Position + count < guid_table_start) {
// entire read before guid table
return stream.Read (buffer, offset, count);
} else if (stream.Position >= guid_table_start + guid_table_length) {
// entire read after guid table
return stream.Read (buffer, offset, count);
} else {
int read = 0;
// read up intil guid table
read = stream.Read (buffer, offset, (int) (guid_table_start - stream.Position));
// skip guid table
stream.Position += guid_table_length;
// read after guid table
if (count - read > 0)
read += stream.Read (buffer, offset + read, count - read);
return read;
}
}
void FindGUIDTable ()
{
using (var fs = File.OpenRead (filename)) {
using (var str = new BinaryReader (fs)) {
str.BaseStream.Position = 0;
if (str.ReadByte () != 0x4d || str.ReadByte () != 0x5a)
return; // MZ header
str.BaseStream.Position = 0x80;
if (str.ReadByte () != 'P' || str.ReadByte () != 'E' || str.ReadByte () != 0 || str.ReadByte () != 0)
return; // PE signature ("PE\0\0")
// Read the PE file header
if (str.ReadByte () != 0x4c || str.ReadByte () != 0x01)
return; // PE file header -> Machine (always 0x014c)
ushort sectionCount = str.ReadUInt16 ();
str.BaseStream.Position += 12;
ushort optionalHeaderSize = str.ReadUInt16 ();
if (optionalHeaderSize < 224)
return; // optional header is not big enough
str.BaseStream.Position += 2;
// Read the optional PE header
str.BaseStream.Position += 208;
int cliHeaderRVA = str.ReadInt32 ();
/*int cliHeaderSize = */str.ReadInt32 ();
str.BaseStream.Position += 8;
// Read the sections, looking for the ".text" section.
int sectionHeaderPosition = (int) str.BaseStream.Position;
int textSectionPosition = -1;
uint virtualAddress = uint.MaxValue;
uint pointerToRawData = 0;
for (int i = 0; i < sectionCount; i++) {
str.BaseStream.Position = sectionHeaderPosition + 40 * i;
if (str.ReadByte () != '.' || str.ReadByte () != 't' || str.ReadByte () != 'e' || str.ReadByte () != 'x' || str.ReadByte () != 't' || str.ReadByte () != 0)
continue;
textSectionPosition = sectionHeaderPosition + 40 * i;
str.BaseStream.Position = textSectionPosition + 12;
virtualAddress = str.ReadUInt32 ();
str.BaseStream.Position += 4;
pointerToRawData = str.ReadUInt32 ();
break;
}
if (virtualAddress == uint.MaxValue)
return;
// Now we can calculate the file position of the CLI header
str.BaseStream.Position = cliHeaderRVA - (virtualAddress - pointerToRawData);
str.BaseStream.Position += 8;
uint metadataRVA = str.ReadUInt32 ();
/*uint metadataSize = */str.ReadUInt32 ();
// Find and read the metadata header
uint metadataRootPosition = metadataRVA - (virtualAddress - pointerToRawData);
str.BaseStream.Position = metadataRootPosition;
if (str.ReadByte () != 0x42 || str.ReadByte () != 0x53 || str.ReadByte () != 0x4a || str.ReadByte () != 0x42)
return; // Invalid magic signature.
str.BaseStream.Position += 8;
int dynamicLength = str.ReadInt32 ();
str.BaseStream.Position += dynamicLength;
str.BaseStream.Position += 2; // flags
ushort metadataStreams = str.ReadUInt16 ();
for (ushort i = 0; i < metadataStreams; i++) {
uint offset = str.ReadUInt32 ();
uint size = str.ReadUInt32 ();
byte[] name = new byte [32];
for (int k = 0; k < 8; k++) {
str.Read (name, k * 4, 4);
if (name [k * 4 + 3] == 0)
break;
}
if (name [0] == '#' && name [1] == 'G' && name [2] == 'U' && name [3] == 'I' && name [4] == 'D' && name [5] == 0) {
// found the GUID table.
guid_table_start = metadataRootPosition + offset;
guid_table_length = size;
return;
}
}
}
}
}
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
stream.Dispose ();
}
public override void Flush ()
{
throw new NotImplementedException ();
}
public override long Seek (long offset, SeekOrigin origin)
{
throw new NotImplementedException ();
}
public override void SetLength (long value)
{
throw new NotImplementedException ();
}
public override void Write (byte[] buffer, int offset, int count)
{
throw new NotImplementedException ();
}
public override bool CanRead {
get {
throw new NotImplementedException ();
}
}
public override bool CanSeek {
get {
throw new NotImplementedException ();
}
}
public override bool CanWrite {
get {
throw new NotImplementedException ();
}
}
public override long Length {
get {
return stream.Length - 140 - guid_table_length;
}
}
public override long Position {
get {
throw new NotImplementedException ();
}
set {
throw new NotImplementedException ();
}
}
}
#if false
static public void ComputeDependencies (IEnumerable<string> assemblies, MonoTouchResolver resolver)
{
// note: Parallel.ForEach (with lock to add on 'digests') turns out (much) slower
// (linksdk.app with 20 assemblies)
// likely because it's faster (using commoncrypto) than it seems
foreach (string a in assemblies) {
string key = Path.GetFileNameWithoutExtension (a);
using (Stream fs = File.OpenRead (a)) {
string digest = ComputeDigest (fs, 140);
digests.Add (key, digest);
}
}
Dictionary<string, HashSet<string>> dependencies = new Dictionary<string, HashSet<string>> ();
foreach (string a in assemblies) {
HashSet<string> references;
AssemblyDefinition ad = resolver.Load (a);
foreach (AssemblyNameReference ar in ad.MainModule.AssemblyReferences) {
if (!dependencies.TryGetValue (ar.Name, out references)) {
references = new HashSet<string> ();
dependencies.Add (ar.Name, references);
}
references.Add (ad.Name.Name);
}
}
#if DEBUG
foreach (var kvp in dependencies) {
Console.WriteLine ("The following assemblies depends on {0}", kvp.Key);
foreach (var s in kvp.Value)
Console.WriteLine ("\t{0}", s);
}
#endif
// if a dependency has changed everything that depends on it must be cleaned
foreach (var kvp in dependencies) {
string cname = kvp.Key + ".*.cache." + GetDigestForAssembly (kvp.Key) + ".o";
var files = Directory.GetFiles (Location, cname);
if (files.Length != 0)
continue;
Clean (kvp.Key + "*");
foreach (var deps in kvp.Value)
Clean (deps + "*");
}
}
#endif
}