From e26ee5ca6cc7ad5961703ce90fc8f9d6c3db374c Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Thu, 12 Jan 2017 14:49:30 -0500 Subject: [PATCH] [msbuild] Refactor the IBTool task to be cleaner and more efficient (#1457) [msbuild] Refactor the IBTool task to be cleaner and more efficient Besides making the IBTool task cleaner and easier to understand, a side-effect of this refactor was also to optimize the collecting of the compiled outputs because we now only need to scan the obj/ibtool directory once to collect each of the outputs. --- .../Tasks/ACToolTaskBase.cs | 4 +- .../Tasks/IBToolTaskBase.cs | 478 +++++++++++------- .../Tasks/XcodeCompilerToolTask.cs | 4 +- .../Base.lproj/LaunchScreen.storyboard | 27 + .../Base.lproj/Linked.storyboard | 40 ++ .../Base.lproj/Main.storyboard | 48 ++ .../LinkedAndTranslated/Info.plist | 46 ++ .../en.lproj/Linked.storyboard | 40 ++ .../en.lproj/Main.storyboard | 48 ++ .../TargetTests/TargetTests.cs | 7 +- .../TaskTests/IBToolTaskTests.cs | 173 +++++++ .../Xamarin.iOS.Tasks.Tests.csproj | 1 + 12 files changed, 719 insertions(+), 197 deletions(-) create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/LaunchScreen.storyboard create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Linked.storyboard create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Main.storyboard create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Info.plist create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Linked.storyboard create mode 100644 msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Main.storyboard create mode 100644 msbuild/tests/Xamarin.iOS.Tasks.Tests/TaskTests/IBToolTaskTests.cs diff --git a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ACToolTaskBase.cs b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ACToolTaskBase.cs index 898941cf76..fbc24548f3 100644 --- a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ACToolTaskBase.cs +++ b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/ACToolTaskBase.cs @@ -373,12 +373,10 @@ namespace Xamarin.MacDev.Tasks specs.Save (outputSpecs, true); } - var output = new TaskItem (intermediateBundleDir); - Directory.CreateDirectory (intermediateBundleDir); // Note: Compile() will set the PartialAppManifest property if it is used... - if ((Compile (catalogs.ToArray (), output, manifest)) != 0) + if ((Compile (catalogs.ToArray (), intermediateBundleDir, manifest)) != 0) return false; if (PartialAppManifest != null && !File.Exists (PartialAppManifest.GetMetadata ("FullPath"))) diff --git a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/IBToolTaskBase.cs b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/IBToolTaskBase.cs index 42758a3b38..456501c4c7 100644 --- a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/IBToolTaskBase.cs +++ b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/IBToolTaskBase.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Collections; using System.Collections.Generic; using Microsoft.Build.Framework; @@ -68,11 +69,11 @@ namespace Xamarin.MacDev.Tasks string GetBundleRelativeOutputPath (ITaskItem input) { - // Note: InterfaceDefinition files are *always* installed into the root of the app bundle - // InterfaceDefinition files that are contained within an .lproj translation directory - // will retain the .lproj directory as their parent, but the .lproj directory will be + // Note: InterfaceDefinition files are *always* installed into the root of the app bundle. + // + // InterfaceDefinition files that are contained within a *.lproj translation directory + // will retain the *.lproj directory as their parent, but the *.lproj directory will be // in the root of the app bundle. - //var bundleName = BundleResource.GetLogicalName (ProjectDir, ResourcePrefixes, input); var components = input.ItemSpec.Split (Path.DirectorySeparatorChar); var bundleName = components[components.Length - 1]; if (components.Length > 1 && components[components.Length - 2].EndsWith (".lproj", StringComparison.Ordinal)) @@ -88,6 +89,236 @@ namespace Xamarin.MacDev.Tasks } } + static string GetPathWithoutExtension (string path) + { + int dot = path.LastIndexOf ('.'); + + return path.Substring (0, dot); + } + + IEnumerable GetCompilationDirectoryOutput (string baseOutputDir, IDictionary mapping) + { + var baseOutputDirs = new List (); + + baseOutputDirs.Add (baseOutputDir); + + // Note: all storyboardc's/nib's will be found in the top-level or within a top-level *.lproj dir (if they've been translated) + for (int i = 0; i < baseOutputDirs.Count; i++) { + foreach (var path in Directory.EnumerateFileSystemEntries (baseOutputDirs[i])) { + if (i == 0 && path.EndsWith (".lproj", StringComparison.Ordinal) && Directory.Exists (path)) { + baseOutputDirs.Add (path); + continue; + } + + IDictionary metadata; + + if (!mapping.TryGetValue (path, out metadata)) + continue; + + var compiled = new TaskItem (path, metadata); + + // adjust the LogicalName since the LogicalName metadata is based on the generic output name + // (e.g. it does not include things like ~ipad or ~iphone) + var logicalName = compiled.GetMetadata ("LogicalName"); + var logicalDir = Path.GetDirectoryName (logicalName); + var fileName = Path.GetFileName (path); + + compiled.SetMetadata ("LogicalName", Path.Combine (logicalDir, fileName)); + + yield return compiled; + } + } + + yield break; + } + + IEnumerable GetCompilationOutput (ITaskItem expected) + { + if (IsWatchApp) { + var logicalName = expected.GetMetadata ("LogicalName"); + + foreach (var extension in WatchAppExtensions) { + var path = GetPathWithoutExtension (expected.ItemSpec) + extension; + if (File.Exists (path)) { + var item = new TaskItem (path); + expected.CopyMetadataTo (item); + item.SetMetadata ("LogicalName", GetPathWithoutExtension (logicalName) + extension); + yield return item; + } + } + } + + yield return expected; + } + + static bool LogExists (string path) + { + if (!File.Exists (path)) + return false; + + try { + PDictionary.FromFile (path); + return true; + } catch { + File.Delete (path); + return false; + } + } + + static bool InterfaceDefinitionChanged (ITaskItem interfaceDefinition, ITaskItem log) + { + return !LogExists (log.ItemSpec) || File.GetLastWriteTime (log.ItemSpec) < File.GetLastWriteTime (interfaceDefinition.ItemSpec); + } + + bool CompileInterfaceDefinitions (string baseManifestDir, string baseOutputDir, List compiled, IList manifests, out bool changed) + { + var mapping = new Dictionary (); + var unique = new Dictionary (); + var targets = GetTargetDevices (plist).ToList (); + + changed = false; + + foreach (var item in InterfaceDefinitions) { + var bundleName = GetBundleRelativeOutputPath (item); + var manifest = new TaskItem (Path.Combine (baseManifestDir, bundleName)); + var manifestDir = Path.GetDirectoryName (manifest.ItemSpec); + ITaskItem duplicate; + string output; + + if (!File.Exists (item.ItemSpec)) { + Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, "The file '{0}' does not exist.", item.ItemSpec); + continue; + } + + if (unique.TryGetValue (bundleName, out duplicate)) { + Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, "The file '{0}' conflicts with '{1}'.", item.ItemSpec, duplicate.ItemSpec); + continue; + } + + unique.Add (bundleName, item); + + var resourceTags = item.GetMetadata ("ResourceTags"); + var path = Path.Combine (baseOutputDir, bundleName); + var outputDir = Path.GetDirectoryName (path); + var name = GetPathWithoutExtension (path); + var extension = Path.GetExtension (path); + var expected = new TaskItem (path); + + expected.SetMetadata ("InterfaceDefinition", item.ItemSpec); + expected.SetMetadata ("LogicalName", bundleName); + expected.SetMetadata ("Optimize", "false"); + + if (!string.IsNullOrEmpty (resourceTags)) + expected.SetMetadata ("ResourceTags", resourceTags); + + if (UseCompilationDirectory) { + // Note: When using --compilation-directory, we need to specify the output path as the parent directory + output = Path.GetDirectoryName (path); + } else { + output = expected.ItemSpec; + } + + if (InterfaceDefinitionChanged (item, manifest)) { + Directory.CreateDirectory (manifestDir); + Directory.CreateDirectory (outputDir); + + if ((Compile (new[] { item }, output, manifest)) != 0) + return false; + + changed = true; + } else { + Log.LogMessage (MessageImportance.Low, "Skipping `{0}' as the output file, `{1}', is newer.", item.ItemSpec, manifest.ItemSpec); + } + + try { + var dict = PDictionary.FromFile (manifest.ItemSpec); + + LogWarningsAndErrors (dict, item); + } catch (Exception ex) { + Log.LogError ("Failed to load output log file for {0}: {1}", ToolName, ex.Message); + if (File.Exists (manifest.ItemSpec)) + Log.LogError ("ibtool log: {0}", File.ReadAllText (manifest.ItemSpec)); + continue; + } + + if (UseCompilationDirectory) { + // Note: When using a compilation-directory, we'll scan dir the baseOutputDir later as + // an optimization to collect all of the compiled output in one fell swoop. + var metadata = expected.CloneCustomMetadata (); + + foreach (var target in targets) + mapping.Add (name + "~" + target + extension, metadata); + + mapping.Add (path, metadata); + } else { + compiled.AddRange (GetCompilationOutput (expected)); + } + + manifests.Add (manifest); + } + + if (UseCompilationDirectory) + compiled.AddRange (GetCompilationDirectoryOutput (baseOutputDir, mapping)); + + return !Log.HasLoggedErrors; + } + + bool LinkStoryboards (string baseManifestDir, string baseOutputDir, List storyboards, List linked, IList manifests, bool changed) + { + var manifest = new TaskItem (Path.Combine (baseManifestDir, "link")); + var mapping = new Dictionary (); + var unique = new HashSet (); + var items = new List (); + + // Make sure that `Main.storyboardc` is listed *before* `Main~ipad.storyboardc` and `Main~iphone.storyboardc`, + // this is important for the next step to filter out the device-specific storyboards based on the same source. + storyboards.Sort ((x, y) => string.Compare (x.ItemSpec, y.ItemSpec, StringComparison.Ordinal)); + + // Populate our metadata mapping table so we can properly restore the metadata to the linked items. + // + // While we are at it, we'll also filter out device-specific storyboards since ibtool doesn't + // require them if we have an equivalent generic version. + for (int i = 0; i < storyboards.Count; i++) { + var interfaceDefinition = storyboards[i].GetMetadata ("InterfaceDefinition"); + var bundleName = storyboards[i].GetMetadata ("LogicalName"); + var path = Path.Combine (baseOutputDir, bundleName); + + storyboards[i].RemoveMetadata ("InterfaceDefinition"); + var metadata = storyboards[i].CloneCustomMetadata (); + mapping.Add (path, metadata); + + if (unique.Add (interfaceDefinition)) + items.Add (storyboards[i]); + } + + // We only need to run `ibtool --link` if storyboards have changed... + if (changed) { + if (Directory.Exists (baseOutputDir)) + Directory.Delete (baseOutputDir, true); + + if (File.Exists (manifest.ItemSpec)) + File.Delete (manifest.ItemSpec); + + Directory.CreateDirectory (baseManifestDir); + Directory.CreateDirectory (baseOutputDir); + + try { + Link = true; + + if ((Compile (items.ToArray (), baseOutputDir, manifest)) != 0) + return false; + } finally { + Link = false; + } + } + + linked.AddRange (GetCompilationDirectoryOutput (baseOutputDir, mapping)); + + manifests.Add (manifest); + + return true; + } + IEnumerable RecursivelyEnumerateFiles (ITaskItem output) { var nibDir = output.GetMetadata ("LogicalName"); @@ -120,106 +351,36 @@ namespace Xamarin.MacDev.Tasks yield break; } - static string GetPathWithoutExtension (string path) + IEnumerable GetBundleResources (ITaskItem compiledItem) { - int dot = path.LastIndexOf ('.'); + var baseLogicalName = compiledItem.GetMetadata ("LogicalName"); + var baseDir = compiledItem.ItemSpec; - return path.Substring (0, dot); - } + // Note: Watch App storyboards will be compiled to something like Interface.storyboardc/Interface.plist, but + // Interface.plist needs to be moved up 1 level (e.g. drop the "Interface.storyboardc"). + // See https://bugzilla.xamarin.com/show_bug.cgi?id=33853 for details + if (IsWatchApp && baseLogicalName.EndsWith (".storyboardc", StringComparison.Ordinal)) + baseLogicalName = Path.GetDirectoryName (baseLogicalName); - IEnumerable GetCompilationDirectoryOutput (ITaskItem expected) - { - var dir = Path.GetDirectoryName (expected.ItemSpec); + foreach (var path in Directory.EnumerateFiles (baseDir, "*.*", SearchOption.AllDirectories)) { + var rpath = PathUtils.AbsoluteToRelative (baseDir, Path.GetFullPath (path)); + var bundleResource = new TaskItem (path); + string logicalName; - if (!Directory.Exists (dir)) - yield break; + if (!string.IsNullOrEmpty (baseLogicalName)) + logicalName = Path.Combine (baseLogicalName, rpath); + else + logicalName = rpath; - var name = Path.GetFileNameWithoutExtension (expected.ItemSpec); - var extension = Path.GetExtension (expected.ItemSpec); - var nibDir = expected.GetMetadata ("LogicalName"); - var targets = GetTargetDevices (plist).ToList (); + compiledItem.CopyMetadataTo (bundleResource); + bundleResource.SetMetadata ("LogicalName", logicalName); - foreach (var path in Directory.GetFileSystemEntries (dir)) { - // check that the FileNameWithoutExtension matches *exactly* - if (string.Compare (path, dir.Length + 1, name, 0, name.Length) != 0) - continue; - - int startIndex = dir.Length + 1 + name.Length; - - // match against files that have a "~" + $target (iphone, ipad, etc) - if (path.Length > startIndex && path[startIndex] == '~') { - bool matched = false; - - startIndex++; - - foreach (var target in targets) { - // Note: we match the target case-insensitively because of https://bugzilla.xamarin.com/show_bug.cgi?id=44811 - if (string.Compare (path, startIndex, target, 0, target.Length, StringComparison.OrdinalIgnoreCase) == 0) { - startIndex += target.Length; - matched = true; - break; - } - } - - if (!matched) - continue; - } - - // at this point, all that should be left is the file/directory extension - if (path.Length != startIndex + extension.Length || string.Compare (path, startIndex, extension, 0, extension.Length) != 0) - continue; - - var fileName = Path.GetFileName (path); - var logicalName = !string.IsNullOrEmpty (nibDir) ? Path.Combine (nibDir, fileName) : fileName; - var item = new TaskItem (path); - expected.CopyMetadataTo (item); - item.SetMetadata ("LogicalName", logicalName); - - yield return item; + yield return bundleResource; } yield break; } - IEnumerable GetCompiledBundleResources (ITaskItem output) - { - if (IsWatchApp && !UseCompilationDirectory) { - var logicalName = output.GetMetadata ("LogicalName"); - - foreach (var extension in WatchAppExtensions) { - var path = GetPathWithoutExtension (output.ItemSpec) + extension; - if (File.Exists (path)) { - var item = new TaskItem (path); - item.SetMetadata ("LogicalName", GetPathWithoutExtension (logicalName) + extension); - item.SetMetadata ("Optimize", "false"); - yield return item; - } - } - } else if (Directory.Exists (output.ItemSpec)) { - // Note: historically, only storyboard files compiled to directories containing the real nib files, but the new iOS 8 .xib's do as well. - foreach (var file in RecursivelyEnumerateFiles (output)) - yield return file; - - yield break; - } - - yield return output; - } - - static bool ManifestExists (string path) - { - if (!File.Exists (path)) - return false; - - try { - PDictionary.FromFile (path); - return true; - } catch { - File.Delete (path); - return false; - } - } - public override bool Execute () { Log.LogTaskName ("IBTool"); @@ -242,12 +403,11 @@ namespace Xamarin.MacDev.Tasks return !Log.HasLoggedErrors; } - var ibtoolManifestDir = Path.Combine (IntermediateOutputPath, ToolName + "-manifests"); - var ibtoolOutputDir = Path.Combine (IntermediateOutputPath, ToolName); - var bundleResources = new List (); + var ibtoolManifestDir = Path.Combine (IntermediateOutputPath, "ibtool-manifests"); + var ibtoolOutputDir = Path.Combine (IntermediateOutputPath, "ibtool"); var outputManifests = new List (); var compiled = new List (); - bool changed = false; + bool changed; if (InterfaceDefinitions.Length > 0) { if (AppManifest != null) { @@ -264,105 +424,51 @@ namespace Xamarin.MacDev.Tasks Directory.CreateDirectory (ibtoolManifestDir); Directory.CreateDirectory (ibtoolOutputDir); - } - - foreach (var item in InterfaceDefinitions) { - var bundleName = GetBundleRelativeOutputPath (item); - var manifest = new TaskItem (Path.Combine (ibtoolManifestDir, bundleName)); - var manifestDir = Path.GetDirectoryName (manifest.ItemSpec); - var resourceTags = item.GetMetadata ("ResourceTags"); - ITaskItem expected, output; - string rpath, outputDir; - if (!File.Exists (item.ItemSpec)) { - Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, "The file '{0}' does not exist.", item.ItemSpec); - continue; - } + if (!CompileInterfaceDefinitions (ibtoolManifestDir, ibtoolOutputDir, compiled, outputManifests, out changed)) + return false; - rpath = Path.Combine (ibtoolOutputDir, bundleName); - outputDir = Path.GetDirectoryName (rpath); - expected = new TaskItem (rpath); + if (CanLinkStoryboards) { + var storyboards = new List (); + var linked = new List (); + var unique = new HashSet (); - expected.SetMetadata ("LogicalName", bundleName); - expected.SetMetadata ("Optimize", "false"); + for (int i = 0; i < compiled.Count; i++) { + // pretend that non-storyboardc items (e.g. *.nib) are already 'linked' + if (compiled[i].ItemSpec.EndsWith (".storyboardc", StringComparison.Ordinal)) { + var interfaceDefinition = compiled[i].GetMetadata ("InterfaceDefinition"); + unique.Add (interfaceDefinition); + storyboards.Add (compiled[i]); + continue; + } - if (!string.IsNullOrEmpty (resourceTags)) - expected.SetMetadata ("ResourceTags", resourceTags); + // just pretend any *nib's have already been 'linked'... + compiled[i].RemoveMetadata ("InterfaceDefinition"); + linked.Add (compiled[i]); + } - if (UseCompilationDirectory) { - // Note: When using --compilation-directory, we need to specify the output path as the parent directory - output = new TaskItem (expected); - output.ItemSpec = Path.GetDirectoryName (output.ItemSpec); - output.SetMetadata ("LogicalName", Path.GetDirectoryName (bundleName)); + // only link the storyboards if there are multiple unique storyboards + if (unique.Count > 1) { + var linkOutputDir = Path.Combine (IntermediateOutputPath, "ibtool-link"); + + if (!LinkStoryboards (ibtoolManifestDir, linkOutputDir, storyboards, linked, outputManifests, changed)) + return false; + + compiled = linked; + } } else { - output = expected; + for (int i = 0; i < compiled.Count; i++) + compiled[i].RemoveMetadata ("InterfaceDefinition"); } - - if (!ManifestExists (manifest.ItemSpec) || File.GetLastWriteTime (manifest.ItemSpec) < File.GetLastWriteTime (item.ItemSpec)) { - Directory.CreateDirectory (manifestDir); - Directory.CreateDirectory (outputDir); - - if ((Compile (new[] { item }, output, manifest)) != 0) - return false; - - changed = true; - } else { - Log.LogMessage (MessageImportance.Low, "Skipping `{0}' as the output file, `{1}', is newer.", item.ItemSpec, manifest.ItemSpec); - } - - try { - var dict = PDictionary.FromFile (manifest.ItemSpec); - - LogWarningsAndErrors (dict, item); - } catch (Exception ex) { - Log.LogError ("Failed to load output manifest for {0}: {1}", ToolName, ex.Message); - if (File.Exists (manifest.ItemSpec)) - Log.LogError ("Output manifest contents: {0}", File.ReadAllText (manifest.ItemSpec)); - continue; - } - - if (UseCompilationDirectory) - compiled.AddRange (GetCompilationDirectoryOutput (expected)); - else - bundleResources.AddRange (GetCompiledBundleResources (output)); - - outputManifests.Add (manifest); } - if (InterfaceDefinitions.Length > 0 && UseCompilationDirectory) { - var output = new TaskItem (ibtoolOutputDir); - output.SetMetadata ("LogicalName", ""); + var bundleResources = new List (); - if (!CanLinkStoryboards) - bundleResources.AddRange (GetCompiledBundleResources (output)); - } - - if (CanLinkStoryboards && compiled.Count > 0) { - var linkOutputDir = Path.Combine (IntermediateOutputPath, ToolName + "-link"); - var manifest = new TaskItem (Path.Combine (ibtoolManifestDir, "link")); - var output = new TaskItem (linkOutputDir); - - if (changed) { - if (Directory.Exists (output.ItemSpec)) - Directory.Delete (output.ItemSpec, true); - - if (File.Exists (manifest.ItemSpec)) - File.Delete (manifest.ItemSpec); - - Directory.CreateDirectory (Path.GetDirectoryName (manifest.ItemSpec)); - Directory.CreateDirectory (output.ItemSpec); - - Link = true; - - if ((Compile (compiled.ToArray (), output, manifest)) != 0) - return false; - } - - output = new TaskItem (linkOutputDir); - output.SetMetadata ("LogicalName", ""); - - bundleResources.AddRange (GetCompiledBundleResources (output)); - outputManifests.Add (manifest); + foreach (var compiledItem in compiled) { + if (Directory.Exists (compiledItem.ItemSpec)) + bundleResources.AddRange (GetBundleResources (compiledItem)); + else if (File.Exists (compiledItem.ItemSpec)) + bundleResources.Add (compiledItem); } BundleResources = bundleResources.ToArray (); diff --git a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/XcodeCompilerToolTask.cs b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/XcodeCompilerToolTask.cs index 4cd1366fff..1a0e542356 100644 --- a/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/XcodeCompilerToolTask.cs +++ b/msbuild/Xamarin.MacDev.Tasks.Core/Tasks/XcodeCompilerToolTask.cs @@ -124,7 +124,7 @@ namespace Xamarin.MacDev.Tasks return startInfo; } - protected int Compile (ITaskItem[] items, ITaskItem output, ITaskItem manifest) + protected int Compile (ITaskItem[] items, string output, ITaskItem manifest) { var environment = new Dictionary (); var args = new ProcessArgumentBuilder (); @@ -147,7 +147,7 @@ namespace Xamarin.MacDev.Tasks else args.Add ("--compile"); - args.AddQuoted (output.GetMetadata ("FullPath")); + args.AddQuoted (Path.GetFullPath (output)); foreach (var item in items) args.AddQuoted (item.GetMetadata ("FullPath")); diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/LaunchScreen.storyboard b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..fdf3f97d1b --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Linked.storyboard b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Linked.storyboard new file mode 100644 index 0000000000..95ba12afa3 --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Linked.storyboard @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Main.storyboard b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..02041eec03 --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Base.lproj/Main.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Info.plist b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Info.plist new file mode 100644 index 0000000000..7eaa49f6ce --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + IBToolTaskTest + CFBundleIdentifier + com.your-company.ibtooltasktest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + IBToolTaskTest + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + MinimumOSVersion + 10.1 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Linked.storyboard b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Linked.storyboard new file mode 100644 index 0000000000..95ba12afa3 --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Linked.storyboard @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Main.storyboard b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Main.storyboard new file mode 100644 index 0000000000..02041eec03 --- /dev/null +++ b/msbuild/tests/IBToolTaskTests/LinkedAndTranslated/en.lproj/Main.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msbuild/tests/Xamarin.iOS.Tasks.Tests/TargetTests/TargetTests.cs b/msbuild/tests/Xamarin.iOS.Tasks.Tests/TargetTests/TargetTests.cs index 88a34d52b2..10bd96f600 100644 --- a/msbuild/tests/Xamarin.iOS.Tasks.Tests/TargetTests/TargetTests.cs +++ b/msbuild/tests/Xamarin.iOS.Tasks.Tests/TargetTests/TargetTests.cs @@ -433,14 +433,9 @@ namespace Xamarin.iOS.Tasks public void BundleResources () { var actool = Path.Combine ("obj", "iPhoneSimulator", "Debug", "actool", "bundle"); + var ibtool = Path.Combine ("obj", "iPhoneSimulator", "Debug", "ibtool"); var path = Path.Combine (MonoTouchProjectPath, "Info.plist"); var plist = PDictionary.FromFile (path); - string ibtool; - - if (AppleSdkSettings.XcodeVersion.Major >= 7) - ibtool = Path.Combine ("obj", "iPhoneSimulator", "Debug", "ibtool-link"); - else - ibtool = Path.Combine ("obj", "iPhoneSimulator", "Debug", "ibtool"); plist.SetMinimumOSVersion ("6.1"); plist.Save (path, true); diff --git a/msbuild/tests/Xamarin.iOS.Tasks.Tests/TaskTests/IBToolTaskTests.cs b/msbuild/tests/Xamarin.iOS.Tasks.Tests/TaskTests/IBToolTaskTests.cs new file mode 100644 index 0000000000..9784d6c6ac --- /dev/null +++ b/msbuild/tests/Xamarin.iOS.Tasks.Tests/TaskTests/IBToolTaskTests.cs @@ -0,0 +1,173 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using NUnit.Framework; + +using Xamarin.MacDev; +using Xamarin.MacDev.Tasks; + +namespace Xamarin.iOS.Tasks +{ + [TestFixture] + public class IBToolTaskTests + { + static IBTool CreateIBToolTask (PlatformFramework framework, string projectDir, string intermediateOutputPath) + { + var interfaceDefinitions = new List (); + var sdk = IPhoneSdks.GetSdk (framework); + var version = IPhoneSdkVersion.GetDefault (sdk, false); + var root = sdk.GetSdkPath (version, false); + var usr = Path.Combine (sdk.DeveloperRoot, "usr"); + var bin = Path.Combine (usr, "bin"); + string platform; + + switch (framework) { + case PlatformFramework.WatchOS: + platform = "WatchOS"; + break; + case PlatformFramework.TVOS: + platform = "AppleTVOS"; + break; + default: + platform = "iPhoneOS"; + break; + } + + foreach (var item in Directory.EnumerateFiles (projectDir, "*.storyboard", SearchOption.AllDirectories)) + interfaceDefinitions.Add (new TaskItem (item)); + + foreach (var item in Directory.EnumerateFiles (projectDir, "*.xib", SearchOption.AllDirectories)) + interfaceDefinitions.Add (new TaskItem (item)); + + return new IBTool { + AppManifest = new TaskItem (Path.Combine (projectDir, "Info.plist")), + InterfaceDefinitions = interfaceDefinitions.ToArray (), + IntermediateOutputPath = intermediateOutputPath, + BuildEngine = new TestEngine (), + ResourcePrefix = "Resources", + ProjectDir = projectDir, + SdkPlatform = platform, + SdkVersion = version.ToString (), + SdkUsrPath = usr, + SdkBinPath = bin, + SdkRoot = root, + }; + } + + [Test] + public void TestBasicIBToolFunctionality () + { + var tmp = Path.Combine (Path.GetTempPath (), "basic-ibtool"); + + Directory.CreateDirectory (tmp); + + try { + var ibtool = CreateIBToolTask (PlatformFramework.iOS, "../MyIBToolLinkTest", tmp); + var bundleResources = new HashSet (); + + Assert.IsTrue (ibtool.Execute (), "Execution of IBTool task failed."); + + foreach (var bundleResource in ibtool.BundleResources) { + Assert.IsTrue (File.Exists (bundleResource.ItemSpec), "File does not exist: {0}", bundleResource.ItemSpec); + Assert.IsNotNullOrEmpty (bundleResource.GetMetadata ("LogicalName"), "The 'LogicalName' metadata must be set."); + Assert.IsNotNullOrEmpty (bundleResource.GetMetadata ("Optimize"), "The 'Optimize' metadata must be set."); + + bundleResources.Add (bundleResource.GetMetadata ("LogicalName")); + } + + string[] expected = { "LaunchScreen~ipad.nib/objects-8.0+.nib", + "LaunchScreen~ipad.nib/runtime.nib", + "LaunchScreen~iphone.nib/objects-8.0+.nib", + "LaunchScreen~iphone.nib/runtime.nib", + "Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC~ipad.nib/objects-8.0+.nib", + "Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC~ipad.nib/runtime.nib", + "Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC~iphone.nib/objects-8.0+.nib", + "Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC~iphone.nib/runtime.nib", + "Main.storyboardc/UIViewController-BYZ-38-t0r~ipad.nib/objects-8.0+.nib", + "Main.storyboardc/UIViewController-BYZ-38-t0r~ipad.nib/runtime.nib", + "Main.storyboardc/UIViewController-BYZ-38-t0r~iphone.nib/objects-8.0+.nib", + "Main.storyboardc/UIViewController-BYZ-38-t0r~iphone.nib/runtime.nib", + "Main~ipad.storyboardc/Info-8.0+.plist", + "Main~ipad.storyboardc/Info.plist", + "Main~iphone.storyboardc/Info-8.0+.plist", + "Main~iphone.storyboardc/Info.plist" + }; + + foreach (var bundleResource in expected) + Assert.IsTrue (bundleResources.Contains (bundleResource), "BundleResources should include '{0}'", bundleResource); + + Assert.AreEqual (expected.Length, bundleResources.Count, "Unexpected number of BundleResources"); + } finally { + Directory.Delete (tmp, true); + } + } + + [Test] + public void TestAdvancedIBToolFunctionality () + { + var tmp = Path.Combine (Path.GetTempPath (), "advanced-ibtool"); + IBTool ibtool; + + Directory.CreateDirectory (tmp); + + try { + ibtool = CreateIBToolTask (PlatformFramework.iOS, "../IBToolTaskTests/LinkedAndTranslated", tmp); + var bundleResources = new HashSet (); + + // Add some ResourceTags... + foreach (var storyboard in ibtool.InterfaceDefinitions) { + var tag = Path.GetFileNameWithoutExtension (storyboard.ItemSpec); + storyboard.SetMetadata ("ResourceTags", tag); + } + + Assert.IsTrue (ibtool.Execute (), "Execution of IBTool task failed."); + + foreach (var bundleResource in ibtool.BundleResources) { + var bundleName = bundleResource.GetMetadata ("LogicalName"); + var tag = bundleResource.GetMetadata ("ResourceTags"); + + Assert.IsTrue (File.Exists (bundleResource.ItemSpec), "File does not exist: {0}", bundleResource.ItemSpec); + Assert.IsNotNullOrEmpty (bundleResource.GetMetadata ("LogicalName"), "The 'LogicalName' metadata must be set."); + Assert.IsNotNullOrEmpty (bundleResource.GetMetadata ("Optimize"), "The 'Optimize' metadata must be set."); + + Assert.IsNotNullOrEmpty (tag, "The 'ResourceTags' metadata should be set."); + Assert.IsTrue (bundleName.Contains (".lproj/" + tag + ".storyboardc/"), "BundleResource does not have the proper ResourceTags set: {0}", bundleName); + + bundleResources.Add (bundleName); + } + + string[] expected = { + "Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib", + "Base.lproj/LaunchScreen.storyboardc/Info.plist", + "Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib", + "Base.lproj/Linked.storyboardc/5xv-Yx-H4r-view-gMo-tm-chA.nib", + "Base.lproj/Linked.storyboardc/Info.plist", + "Base.lproj/Linked.storyboardc/MyLinkedViewController.nib", + "Base.lproj/Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC.nib", + "Base.lproj/Main.storyboardc/Info.plist", + "Base.lproj/Main.storyboardc/MyLinkedViewController.nib", + "Base.lproj/Main.storyboardc/UIViewController-BYZ-38-t0r.nib", + "en.lproj/Linked.storyboardc/5xv-Yx-H4r-view-gMo-tm-chA.nib", + "en.lproj/Linked.storyboardc/Info.plist", + "en.lproj/Linked.storyboardc/MyLinkedViewController.nib", + "en.lproj/Main.storyboardc/BYZ-38-t0r-view-8bC-Xf-vdC.nib", + "en.lproj/Main.storyboardc/Info.plist", + "en.lproj/Main.storyboardc/MyLinkedViewController.nib", + "en.lproj/Main.storyboardc/UIViewController-BYZ-38-t0r.nib" + }; + + foreach (var bundleResource in expected) + Assert.IsTrue (bundleResources.Contains (bundleResource), "BundleResources should include '{0}'", bundleResource); + + Assert.AreEqual (expected.Length, bundleResources.Count, "Unexpected number of BundleResources"); + } finally { + Directory.Delete (tmp, true); + } + } + } +} diff --git a/msbuild/tests/Xamarin.iOS.Tasks.Tests/Xamarin.iOS.Tasks.Tests.csproj b/msbuild/tests/Xamarin.iOS.Tasks.Tests/Xamarin.iOS.Tasks.Tests.csproj index a8fccf7577..abee44b639 100644 --- a/msbuild/tests/Xamarin.iOS.Tasks.Tests/Xamarin.iOS.Tasks.Tests.csproj +++ b/msbuild/tests/Xamarin.iOS.Tasks.Tests/Xamarin.iOS.Tasks.Tests.csproj @@ -108,6 +108,7 @@ +