|
| 1 | +#define TRACE |
| 2 | + |
1 | 3 | using System; |
| 4 | +using System.Diagnostics; |
2 | 5 | using System.Diagnostics.CodeAnalysis; |
3 | 6 | using System.IO; |
4 | 7 | using System.Linq; |
5 | 8 | using System.Collections.Generic; |
6 | 9 |
|
7 | 10 | using Microsoft.Build.Framework; |
| 11 | +using Microsoft.Build.Utilities; |
8 | 12 |
|
| 13 | +using Xamarin.Localization.MSBuild; |
9 | 14 | using Xamarin.Utils; |
| 15 | +using Xamarin.MacDev.Tasks; |
| 16 | + |
| 17 | +#nullable enable |
10 | 18 |
|
11 | 19 | namespace Xamarin.MacDev { |
12 | 20 | public static class BundleResource { |
@@ -48,74 +56,147 @@ public static bool IsIllegalName (string name, [NotNullWhen (true)] out string? |
48 | 56 | return false; |
49 | 57 | } |
50 | 58 |
|
51 | | - public static IList<string> SplitResourcePrefixes (string prefix) |
| 59 | + public static IList<string> SplitResourcePrefixes (string? prefix) |
52 | 60 | { |
| 61 | + if (prefix is null) |
| 62 | + return Array.Empty<string> (); |
| 63 | + |
53 | 64 | return prefix.Split (new [] { ';' }, StringSplitOptions.RemoveEmptyEntries) |
54 | 65 | .Select (s => s.Replace ('\\', Path.DirectorySeparatorChar).Trim () + Path.DirectorySeparatorChar) |
55 | 66 | .Where (s => s.Length > 1) |
56 | 67 | .ToList (); |
57 | 68 | } |
58 | 69 |
|
59 | | - public static string GetVirtualProjectPath (string projectDir, ITaskItem item, bool isVSBuild) |
| 70 | + [Conditional ("TRACE")] |
| 71 | + static void Trace (Task task, string msg) |
60 | 72 | { |
61 | | - var link = item.GetMetadata ("Link"); |
| 73 | + task.Log.LogMessage (MessageImportance.Low, msg); |
| 74 | + } |
62 | 75 |
|
63 | | - // Note: if the Link metadata exists, then it will be the equivalent of the ProjectVirtualPath |
| 76 | + // Compute the path of 'item' relative to the project. |
| 77 | + public static string GetVirtualProjectPath<T> (T task, ITaskItem item) where T: Task, IHasProjectDir, IHasSessionId |
| 78 | + { |
| 79 | + // If the Link metadata exists, use that, it takes precedence over anything else. |
| 80 | + var link = item.GetMetadata ("Link"); |
64 | 81 | if (!string.IsNullOrEmpty (link)) { |
65 | | - if (Path.DirectorySeparatorChar != '\\') |
66 | | - return link.Replace ('\\', '/'); |
67 | | - |
| 82 | + // Canonicalize to use macOS-style directory separators. |
| 83 | + link = link.Replace ('\\', '/'); |
| 84 | + Trace (task, $"BundleResource.GetVirtualProjectPath ({item.ItemSpec}) => Link={link}"); |
68 | 85 | return link; |
69 | 86 | } |
70 | 87 |
|
71 | | - // HACK: This is for Visual Studio iOS projects |
72 | | - if (isVSBuild) { |
73 | | - if (item.GetMetadata ("DefiningProjectFullPath") != item.GetMetadata ("MSBuildProjectFullPath")) { |
74 | | - return item.GetMetadata ("FullPath").Replace (item.GetMetadata ("DefiningProjectDirectory"), string.Empty); |
75 | | - } else { |
76 | | - return item.ItemSpec; |
77 | | - } |
78 | | - } |
| 88 | + // Note that '/' is a valid path separator on Windows (in addition to '\'), so canonicalize the paths to use '/' as the path separator. |
79 | 89 |
|
80 | 90 | var isDefaultItem = item.GetMetadata ("IsDefaultItem") == "true"; |
81 | | - var definingProjectFullPath = item.GetMetadata (isDefaultItem ? "MSBuildProjectFullPath" : "DefiningProjectFullPath"); |
82 | | - var path = item.GetMetadata ("FullPath"); |
83 | | - string baseDir; |
| 91 | + var localMSBuildProjectFullPath = item.GetMetadata ("LocalMSBuildProjectFullPath").Replace ('\\', '/'); |
| 92 | + var localDefiningProjectFullPath = item.GetMetadata ("LocalDefiningProjectFullPath").Replace ('\\', '/'); |
| 93 | + if (string.IsNullOrEmpty (localDefiningProjectFullPath)) { |
| 94 | + task.Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, MSBStrings.E7133 /* The item '{0}'' does not have a '{1}' value set. */, item.ItemSpec, "LocalDefiningProjectFullPath"); |
| 95 | + return "placeholder"; |
| 96 | + } |
| 97 | + |
| 98 | + if (string.IsNullOrEmpty (localMSBuildProjectFullPath)) { |
| 99 | + task.Log.LogError (null, null, null, item.ItemSpec, 0, 0, 0, 0, MSBStrings.E7133 /* The item '{0}'' does not have a '{1}' value set. */, item.ItemSpec, "LocalMSBuildProjectFullPath"); |
| 100 | + return "placeholder"; |
| 101 | + } |
| 102 | + |
| 103 | + // * If we're not a default item, compute the path relative to the |
| 104 | + // file that declared the item in question. |
| 105 | + // * If we're a default item (IsDefaultItem=true), compute |
| 106 | + // relative to the user's project file (because the file that |
| 107 | + // declared the item is our Microsoft.Sdk.DefaultItems.template.props file, |
| 108 | + // and the path relative to that file is certainly not what we want). |
| 109 | + // |
| 110 | + // We use the 'LocalMSBuildProjectFullPath' and |
| 111 | + // 'LocalDefiningProjectFullPath' metadata because the |
| 112 | + // 'MSBuildProjectFullPath' and 'DefiningProjectFullPath' are not |
| 113 | + // necessarily correct when building remotely (the relative path |
| 114 | + // between files might not be the same on macOS once XVS has |
| 115 | + // copied them there, in particular for files outside the project |
| 116 | + // directory). |
| 117 | + // |
| 118 | + // The 'LocalMSBuildProjectFullPath' and 'LocalDefiningProjectFullPath' |
| 119 | + // values are set to the Windows version of 'MSBuildProjectFullPath' |
| 120 | + // and 'DefiningProjectFullPath' when building remotely, and the macOS |
| 121 | + // version when building on macOS. |
| 122 | + |
| 123 | + // First find the absolute path to the item |
| 124 | + var projectAbsoluteDir = task.ProjectDir; |
| 125 | + var isRemoteBuild = !string.IsNullOrEmpty (task.SessionId); |
| 126 | + string itemAbsolutePath; |
| 127 | + if (isRemoteBuild) { |
| 128 | + itemAbsolutePath = PathUtils.PathCombineWindows (projectAbsoluteDir, item.ItemSpec); |
| 129 | + } else { |
| 130 | + itemAbsolutePath = Path.Combine (projectAbsoluteDir, item.ItemSpec); |
| 131 | + } |
| 132 | + var originalItemAbsolutePath = itemAbsolutePath; |
84 | 133 |
|
85 | | - if (!string.IsNullOrEmpty (definingProjectFullPath)) { |
86 | | - baseDir = Path.GetDirectoryName (definingProjectFullPath); |
| 134 | + // Then find the directory we should use to compute the result relative to. |
| 135 | + string relativeToDirectory; // this is an absolute path. |
| 136 | + if (isDefaultItem) { |
| 137 | + relativeToDirectory = Path.GetDirectoryName (localMSBuildProjectFullPath); |
87 | 138 | } else { |
88 | | - baseDir = projectDir; |
| 139 | + relativeToDirectory = Path.GetDirectoryName (localDefiningProjectFullPath); |
89 | 140 | } |
| 141 | + var originalRelativeToDirectory = relativeToDirectory; |
90 | 142 |
|
91 | | - baseDir = PathUtils.ResolveSymbolicLinks (baseDir); |
92 | | - path = PathUtils.ResolveSymbolicLinks (path); |
| 143 | + // On macOS we need to resolve symlinks before computing the relative path. |
| 144 | + if (!isRemoteBuild) { |
| 145 | + relativeToDirectory = PathUtils.ResolveSymbolicLinks (relativeToDirectory); |
| 146 | + itemAbsolutePath = PathUtils.ResolveSymbolicLinks (itemAbsolutePath); |
| 147 | + } |
93 | 148 |
|
94 | | - return PathUtils.AbsoluteToRelative (baseDir, path); |
| 149 | + // Compute the relative path we want to return. |
| 150 | + string rv; |
| 151 | + if (isRemoteBuild) { |
| 152 | + rv = PathUtils.AbsoluteToRelativeWindows (relativeToDirectory, itemAbsolutePath); |
| 153 | + } else { |
| 154 | + rv = PathUtils.AbsoluteToRelative (relativeToDirectory, itemAbsolutePath); |
| 155 | + } |
| 156 | + // Make it a mac-style path |
| 157 | + rv = rv.Replace ('\\', '/'); |
| 158 | + |
| 159 | + Trace (task, $"BundleResource.GetVirtualProjectPath ({item.ItemSpec}) => {rv}\n" + |
| 160 | + $"\t\t\tprojectAbsoluteDir={projectAbsoluteDir}\n" + |
| 161 | + $"\t\t\tIsRemoteBuild={isRemoteBuild}\n" + |
| 162 | + $"\t\t\tisDefaultItem={isDefaultItem}\n" + |
| 163 | + $"\t\t\tLocalMSBuildProjectFullPath={localMSBuildProjectFullPath}\n" + |
| 164 | + $"\t\t\tLocalDefiningProjectFullPath={localDefiningProjectFullPath}\n" + |
| 165 | + $"\t\t\toriginalItemAbsolutePath={originalItemAbsolutePath}\n" + |
| 166 | + $"\t\t\titemAbsolutePath={itemAbsolutePath}\n" + |
| 167 | + $"\t\t\toriginalRelativeToDirectory={originalRelativeToDirectory}\n" + |
| 168 | + $"\t\t\trelativeToDirectory={relativeToDirectory}\n"); |
| 169 | + |
| 170 | + return rv; |
95 | 171 | } |
96 | 172 |
|
97 | | - public static string GetLogicalName (string projectDir, IList<string> prefixes, ITaskItem item, bool isVSBuild) |
| 173 | + public static string GetLogicalName<T> (T task, ITaskItem item) where T: Task, IHasProjectDir, IHasResourcePrefix, IHasSessionId |
98 | 174 | { |
99 | 175 | var logicalName = item.GetMetadata ("LogicalName"); |
100 | 176 |
|
| 177 | + // If an item has the LogicalName metadata set, return that. |
101 | 178 | if (!string.IsNullOrEmpty (logicalName)) { |
102 | | - if (Path.DirectorySeparatorChar != '\\') |
103 | | - return logicalName.Replace ('\\', '/'); |
104 | | - |
105 | | - return logicalName; |
| 179 | + Trace (task, $"BundleResource.GetLogicalName ({item.ItemSpec}) => has LogicalName={logicalName.Replace ('\\', '/')} (original {logicalName})"); |
| 180 | + // Canonicalize to use macOS-style directory separators. |
| 181 | + return logicalName.Replace ('\\', '/'); |
106 | 182 | } |
107 | 183 |
|
108 | | - var vpath = GetVirtualProjectPath (projectDir, item, isVSBuild); |
| 184 | + // Check if the start of the item matches any of the resource prefixes, in which case choose |
| 185 | + // the longest resource prefix, and subtract it from the start of the item. |
| 186 | + var vpath = GetVirtualProjectPath (task, item); |
109 | 187 | int matchlen = 0; |
110 | | - |
| 188 | + var prefixes = SplitResourcePrefixes (task.ResourcePrefix); |
111 | 189 | foreach (var prefix in prefixes) { |
112 | 190 | if (vpath.StartsWith (prefix, StringComparison.OrdinalIgnoreCase) && prefix.Length > matchlen) |
113 | 191 | matchlen = prefix.Length; |
114 | 192 | } |
115 | | - |
116 | | - if (matchlen > 0) |
| 193 | + if (matchlen > 0) { |
| 194 | + Trace (task, $"BundleResource.GetLogicalName ({item.ItemSpec}) => LogicalName={vpath.Substring (matchlen)} (vpath={vpath} matchlen={matchlen} prefixes={string.Join (",", prefixes)})"); |
117 | 195 | return vpath.Substring (matchlen); |
| 196 | + } |
118 | 197 |
|
| 198 | + // Otherwise return the item as-is. |
| 199 | + Trace (task, $"BundleResource.GetLogicalName ({item.ItemSpec}) => LogicalName={vpath} (prefixes={string.Join (",", prefixes)})"); |
119 | 200 | return vpath; |
120 | 201 | } |
121 | 202 | } |
|
0 commit comments