[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.
This commit is contained in:
Jeffrey Stedfast 2017-01-12 14:49:30 -05:00 коммит произвёл GitHub
Родитель cf169d43ac
Коммит e26ee5ca6c
12 изменённых файлов: 719 добавлений и 197 удалений

Просмотреть файл

@ -373,12 +373,10 @@ namespace Xamarin.MacDev.Tasks
specs.Save (outputSpecs, true); specs.Save (outputSpecs, true);
} }
var output = new TaskItem (intermediateBundleDir);
Directory.CreateDirectory (intermediateBundleDir); Directory.CreateDirectory (intermediateBundleDir);
// Note: Compile() will set the PartialAppManifest property if it is used... // 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; return false;
if (PartialAppManifest != null && !File.Exists (PartialAppManifest.GetMetadata ("FullPath"))) if (PartialAppManifest != null && !File.Exists (PartialAppManifest.GetMetadata ("FullPath")))

Просмотреть файл

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
@ -68,11 +69,11 @@ namespace Xamarin.MacDev.Tasks
string GetBundleRelativeOutputPath (ITaskItem input) string GetBundleRelativeOutputPath (ITaskItem input)
{ {
// Note: InterfaceDefinition files are *always* installed into the root of the app bundle // 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 // 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. // in the root of the app bundle.
//var bundleName = BundleResource.GetLogicalName (ProjectDir, ResourcePrefixes, input);
var components = input.ItemSpec.Split (Path.DirectorySeparatorChar); var components = input.ItemSpec.Split (Path.DirectorySeparatorChar);
var bundleName = components[components.Length - 1]; var bundleName = components[components.Length - 1];
if (components.Length > 1 && components[components.Length - 2].EndsWith (".lproj", StringComparison.Ordinal)) 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<ITaskItem> GetCompilationDirectoryOutput (string baseOutputDir, IDictionary<string, IDictionary> mapping)
{
var baseOutputDirs = new List<string> ();
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<ITaskItem> 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<ITaskItem> compiled, IList<ITaskItem> manifests, out bool changed)
{
var mapping = new Dictionary<string, IDictionary> ();
var unique = new Dictionary<string, ITaskItem> ();
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<ITaskItem> storyboards, List<ITaskItem> linked, IList<ITaskItem> manifests, bool changed)
{
var manifest = new TaskItem (Path.Combine (baseManifestDir, "link"));
var mapping = new Dictionary<string, IDictionary> ();
var unique = new HashSet<string> ();
var items = new List<ITaskItem> ();
// 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<ITaskItem> RecursivelyEnumerateFiles (ITaskItem output) IEnumerable<ITaskItem> RecursivelyEnumerateFiles (ITaskItem output)
{ {
var nibDir = output.GetMetadata ("LogicalName"); var nibDir = output.GetMetadata ("LogicalName");
@ -120,106 +351,36 @@ namespace Xamarin.MacDev.Tasks
yield break; yield break;
} }
static string GetPathWithoutExtension (string path) IEnumerable<ITaskItem> 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<ITaskItem> GetCompilationDirectoryOutput (ITaskItem expected) foreach (var path in Directory.EnumerateFiles (baseDir, "*.*", SearchOption.AllDirectories)) {
{ var rpath = PathUtils.AbsoluteToRelative (baseDir, Path.GetFullPath (path));
var dir = Path.GetDirectoryName (expected.ItemSpec); var bundleResource = new TaskItem (path);
string logicalName;
if (!Directory.Exists (dir)) if (!string.IsNullOrEmpty (baseLogicalName))
yield break; logicalName = Path.Combine (baseLogicalName, rpath);
else
logicalName = rpath;
var name = Path.GetFileNameWithoutExtension (expected.ItemSpec); compiledItem.CopyMetadataTo (bundleResource);
var extension = Path.GetExtension (expected.ItemSpec); bundleResource.SetMetadata ("LogicalName", logicalName);
var nibDir = expected.GetMetadata ("LogicalName");
var targets = GetTargetDevices (plist).ToList ();
foreach (var path in Directory.GetFileSystemEntries (dir)) { yield return bundleResource;
// 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 break; yield break;
} }
IEnumerable<ITaskItem> 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 () public override bool Execute ()
{ {
Log.LogTaskName ("IBTool"); Log.LogTaskName ("IBTool");
@ -242,12 +403,11 @@ namespace Xamarin.MacDev.Tasks
return !Log.HasLoggedErrors; return !Log.HasLoggedErrors;
} }
var ibtoolManifestDir = Path.Combine (IntermediateOutputPath, ToolName + "-manifests"); var ibtoolManifestDir = Path.Combine (IntermediateOutputPath, "ibtool-manifests");
var ibtoolOutputDir = Path.Combine (IntermediateOutputPath, ToolName); var ibtoolOutputDir = Path.Combine (IntermediateOutputPath, "ibtool");
var bundleResources = new List<ITaskItem> ();
var outputManifests = new List<ITaskItem> (); var outputManifests = new List<ITaskItem> ();
var compiled = new List<ITaskItem> (); var compiled = new List<ITaskItem> ();
bool changed = false; bool changed;
if (InterfaceDefinitions.Length > 0) { if (InterfaceDefinitions.Length > 0) {
if (AppManifest != null) { if (AppManifest != null) {
@ -264,105 +424,51 @@ namespace Xamarin.MacDev.Tasks
Directory.CreateDirectory (ibtoolManifestDir); Directory.CreateDirectory (ibtoolManifestDir);
Directory.CreateDirectory (ibtoolOutputDir); Directory.CreateDirectory (ibtoolOutputDir);
}
foreach (var item in InterfaceDefinitions) { if (!CompileInterfaceDefinitions (ibtoolManifestDir, ibtoolOutputDir, compiled, outputManifests, out changed))
var bundleName = GetBundleRelativeOutputPath (item); return false;
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)) { if (CanLinkStoryboards) {
Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, "The file '{0}' does not exist.", item.ItemSpec); var storyboards = new List<ITaskItem> ();
continue; var linked = new List<ITaskItem> ();
} var unique = new HashSet<string> ();
rpath = Path.Combine (ibtoolOutputDir, bundleName); for (int i = 0; i < compiled.Count; i++) {
outputDir = Path.GetDirectoryName (rpath); // pretend that non-storyboardc items (e.g. *.nib) are already 'linked'
expected = new TaskItem (rpath); if (compiled[i].ItemSpec.EndsWith (".storyboardc", StringComparison.Ordinal)) {
var interfaceDefinition = compiled[i].GetMetadata ("InterfaceDefinition");
unique.Add (interfaceDefinition);
storyboards.Add (compiled[i]);
continue;
}
expected.SetMetadata ("LogicalName", bundleName); // just pretend any *nib's have already been 'linked'...
expected.SetMetadata ("Optimize", "false"); compiled[i].RemoveMetadata ("InterfaceDefinition");
linked.Add (compiled[i]);
}
if (!string.IsNullOrEmpty (resourceTags)) // only link the storyboards if there are multiple unique storyboards
expected.SetMetadata ("ResourceTags", resourceTags); if (unique.Count > 1) {
var linkOutputDir = Path.Combine (IntermediateOutputPath, "ibtool-link");
if (UseCompilationDirectory) { if (!LinkStoryboards (ibtoolManifestDir, linkOutputDir, storyboards, linked, outputManifests, changed))
// Note: When using --compilation-directory, we need to specify the output path as the parent directory return false;
output = new TaskItem (expected);
output.ItemSpec = Path.GetDirectoryName (output.ItemSpec); compiled = linked;
output.SetMetadata ("LogicalName", Path.GetDirectoryName (bundleName)); }
} else { } 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 bundleResources = new List<ITaskItem> ();
var output = new TaskItem (ibtoolOutputDir);
output.SetMetadata ("LogicalName", "");
if (!CanLinkStoryboards) foreach (var compiledItem in compiled) {
bundleResources.AddRange (GetCompiledBundleResources (output)); if (Directory.Exists (compiledItem.ItemSpec))
} bundleResources.AddRange (GetBundleResources (compiledItem));
else if (File.Exists (compiledItem.ItemSpec))
if (CanLinkStoryboards && compiled.Count > 0) { bundleResources.Add (compiledItem);
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);
} }
BundleResources = bundleResources.ToArray (); BundleResources = bundleResources.ToArray ();

Просмотреть файл

@ -124,7 +124,7 @@ namespace Xamarin.MacDev.Tasks
return startInfo; return startInfo;
} }
protected int Compile (ITaskItem[] items, ITaskItem output, ITaskItem manifest) protected int Compile (ITaskItem[] items, string output, ITaskItem manifest)
{ {
var environment = new Dictionary<string, string> (); var environment = new Dictionary<string, string> ();
var args = new ProcessArgumentBuilder (); var args = new ProcessArgumentBuilder ();
@ -147,7 +147,7 @@ namespace Xamarin.MacDev.Tasks
else else
args.Add ("--compile"); args.Add ("--compile");
args.AddQuoted (output.GetMetadata ("FullPath")); args.AddQuoted (Path.GetFullPath (output));
foreach (var item in items) foreach (var item in items)
args.AddQuoted (item.GetMetadata ("FullPath")); args.AddQuoted (item.GetMetadata ("FullPath"));

Просмотреть файл

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="5xv-Yx-H4r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="lP3-9c-L6b">
<objects>
<viewController storyboardIdentifier="MyLinkedViewController" id="5xv-Yx-H4r" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="vTD-aw-nUj"/>
<viewControllerLayoutGuide type="bottom" id="IyX-Ik-oKG"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="gMo-tm-chA">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Linked!" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Gd9-6P-3AQ">
<rect key="frame" x="159" y="323" width="56" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="J6q-Vo-r8c" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="201" y="102"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="suC-9c-gGU">
<rect key="frame" x="164" y="318" width="46" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<connections>
<segue destination="Xa5-w5-hXO" kind="show" id="1S1-fR-EBd"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
<!--Linked-->
<scene sceneID="brE-JR-pH9">
<objects>
<viewControllerPlaceholder storyboardIdentifier="MyLinkedViewController" storyboardName="Linked" id="Xa5-w5-hXO" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="C8X-qd-oxE" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="522" y="94"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>IBToolTaskTest</string>
<key>CFBundleIdentifier</key>
<string>com.your-company.ibtooltasktest</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>IBToolTaskTest</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>10.1</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

Просмотреть файл

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="5xv-Yx-H4r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="lP3-9c-L6b">
<objects>
<viewController storyboardIdentifier="MyLinkedViewController" id="5xv-Yx-H4r" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="vTD-aw-nUj"/>
<viewControllerLayoutGuide type="bottom" id="IyX-Ik-oKG"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="gMo-tm-chA">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Linked!" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Gd9-6P-3AQ">
<rect key="frame" x="159" y="323" width="56" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="J6q-Vo-r8c" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="201" y="102"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="suC-9c-gGU">
<rect key="frame" x="164" y="318" width="46" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<connections>
<segue destination="Xa5-w5-hXO" kind="show" id="1S1-fR-EBd"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
<!--Linked-->
<scene sceneID="brE-JR-pH9">
<objects>
<viewControllerPlaceholder storyboardIdentifier="MyLinkedViewController" storyboardName="Linked" id="Xa5-w5-hXO" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="C8X-qd-oxE" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="522" y="94"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -433,14 +433,9 @@ namespace Xamarin.iOS.Tasks
public void BundleResources () public void BundleResources ()
{ {
var actool = Path.Combine ("obj", "iPhoneSimulator", "Debug", "actool", "bundle"); 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 path = Path.Combine (MonoTouchProjectPath, "Info.plist");
var plist = PDictionary.FromFile (path); 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.SetMinimumOSVersion ("6.1");
plist.Save (path, true); plist.Save (path, true);

Просмотреть файл

@ -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<ITaskItem> ();
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<string> ();
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<string> ();
// 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);
}
}
}
}

Просмотреть файл

@ -108,6 +108,7 @@
<Compile Include="TaskTests\GeneratePlistTaskTests\GeneratePlistTaskTests_watchOS.cs" /> <Compile Include="TaskTests\GeneratePlistTaskTests\GeneratePlistTaskTests_watchOS.cs" />
<Compile Include="ProjectsTests\ReleaseBuild.cs" /> <Compile Include="ProjectsTests\ReleaseBuild.cs" />
<Compile Include="TaskTests\PropertyListEditorTaskTests.cs" /> <Compile Include="TaskTests\PropertyListEditorTaskTests.cs" />
<Compile Include="TaskTests\IBToolTaskTests.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />