main
cs 230 lines 9.47 KB
Raw
1 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3 namespace WixToolset.BuildTasks
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.IO;
9 using System.Xml;
10 using Microsoft.Build.Framework;
11 using Microsoft.Build.Utilities;
12
13 /// <summary>
14 /// This task assigns Culture metadata to files based on the value of the Culture attribute on the
15 /// WixLocalization element inside the file.
16 /// </summary>
17 public class WixAssignCulture : Task
18 {
19 private const string CultureAttributeName = "Culture";
20 private const string OutputSuffixMetadataName = "OutputSuffix";
21 private const string OutputFolderMetadataName = "OutputFolder";
22 private const string InvariantCultureIdentifier = "neutral";
23 private const string NullCultureIdentifier = "null";
24
25 /// <summary>
26 /// The list of cultures to build. Cultures are specified in the following form:
27 /// primary culture,first fallback culture, second fallback culture;...
28 /// Culture groups are seperated by semi-colons
29 /// Culture precedence within a culture group is evaluated from left to right where fallback cultures are
30 /// separated with commas.
31 /// The first (primary) culture in a culture group will be used as the output sub-folder.
32 /// </summary>
33 public string Cultures { get; set; }
34
35 /// <summary>
36 /// The list of files to apply culture information to.
37 /// </summary>
38 [Required]
39 public ITaskItem[] Files { get; set; }
40
41 /// <summary>
42 /// The files that had culture information applied
43 /// </summary>
44 [Output]
45 public ITaskItem[] CultureGroups { get; private set; }
46
47 /// <summary>
48 /// Applies culture information to the files specified by the Files property.
49 /// This task intentionally does not validate that strings are valid Cultures so that we can support
50 /// psuedo-loc.
51 /// </summary>
52 /// <returns>True upon completion of the task execution.</returns>
53 public override bool Execute()
54 {
55 // First, process the culture group list the user specified in the cultures property
56 var cultureGroups = new List<CultureGroup>();
57
58 if (!String.IsNullOrEmpty(this.Cultures))
59 {
60 // Get rid of extra quotes
61 this.Cultures = this.Cultures.Trim('\"');
62
63 // MSBuild cannnot handle "" items for the invariant culture we require the neutral keyword
64 foreach (var cultureGroupString in this.Cultures.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
65 {
66 var cultureGroup = new CultureGroup(cultureGroupString);
67 cultureGroups.Add(cultureGroup);
68 }
69 }
70 else
71 {
72 // Only process the EmbeddedResource items if cultures was unspecified
73 foreach (var file in this.Files)
74 {
75 // Ignore non-wxls
76 if (!String.Equals(file.GetMetadata("Extension"), ".wxl", StringComparison.OrdinalIgnoreCase))
77 {
78 this.Log.LogError("Unable to retrieve the culture for EmbeddedResource {0}. The file type is not supported.", file.ItemSpec);
79 return false;
80 }
81
82 var wxlFile = new XmlDocument();
83 try
84 {
85 wxlFile.Load(file.ItemSpec);
86 }
87 catch (FileNotFoundException)
88 {
89 this.Log.LogError("Unable to retrieve the culture for EmbeddedResource {0}. The file was not found.", file.ItemSpec);
90 return false;
91 }
92 catch (Exception e)
93 {
94 this.Log.LogError("Unable to retrieve the culture for EmbeddedResource {0}: {1}", file.ItemSpec, e.Message);
95 return false;
96 }
97
98 // Take the culture value and try using it to create a culture.
99 var cultureAttr = wxlFile.DocumentElement.Attributes[WixAssignCulture.CultureAttributeName];
100 var wxlCulture = cultureAttr?.Value ?? String.Empty;
101
102 if (0 == wxlCulture.Length)
103 {
104 // We use a keyword for the invariant culture because MSBuild cannnot handle "" items.
105 wxlCulture = InvariantCultureIdentifier;
106 }
107
108 // We found the culture for the WXL, we now need to determine if it maps to a culture group specified
109 // in the Cultures property or if we need to create a new one.
110 this.Log.LogMessage(MessageImportance.Low, "Culture \"{0}\" from EmbeddedResource {1}.", wxlCulture, file.ItemSpec);
111
112 var cultureGroupExists = false;
113 foreach (var cultureGroup in cultureGroups)
114 {
115 foreach (var culture in cultureGroup.Cultures)
116 {
117 if (String.Equals(wxlCulture, culture, StringComparison.OrdinalIgnoreCase))
118 {
119 cultureGroupExists = true;
120 break;
121 }
122 }
123 }
124
125 // The WXL didn't match a culture group we already have so create a new one.
126 if (!cultureGroupExists)
127 {
128 cultureGroups.Add(new CultureGroup(wxlCulture));
129 }
130 }
131 }
132
133 // If we didn't create any culture groups the culture was unspecificed and no WXLs were included
134 // then build an unlocalized target in the output folder
135 if (cultureGroups.Count == 0)
136 {
137 cultureGroups.Add(new CultureGroup());
138 }
139
140 var cultureGroupItems = new List<TaskItem>();
141
142 if (1 == cultureGroups.Count && 0 == this.Files.Length)
143 {
144 // Maintain old behavior, if only one culturegroup is specified and no WXL, output to the default folder
145 var cultureGroupItem = new TaskItem(cultureGroups[0].ToString());
146 cultureGroupItem.SetMetadata(OutputSuffixMetadataName, cultureGroups[0].OutputSuffix);
147 cultureGroupItem.SetMetadata(OutputFolderMetadataName, CultureGroup.DefaultFolder);
148 cultureGroupItems.Add(cultureGroupItem);
149 }
150 else
151 {
152 foreach (var cultureGroup in cultureGroups)
153 {
154 var cultureGroupItem = new TaskItem(cultureGroup.ToString());
155 cultureGroupItem.SetMetadata(OutputSuffixMetadataName, cultureGroup.OutputSuffix);
156 cultureGroupItem.SetMetadata(OutputFolderMetadataName, cultureGroup.OutputFolder);
157 cultureGroupItems.Add(cultureGroupItem);
158
159 this.Log.LogMessage("Culture: {0}", cultureGroup.ToString());
160 }
161 }
162
163 this.CultureGroups = cultureGroupItems.ToArray();
164 return true;
165 }
166
167 private class CultureGroup
168 {
169 /// <summary>
170 /// TargetPath already has a '\', do not double it!
171 /// </summary>
172 public const string DefaultFolder = "";
173
174 /// <summary>
175 /// Language neutral.
176 /// </summary>
177 public const string DefaultSuffix = InvariantCultureIdentifier;
178
179 /// <summary>
180 /// Initialize a null culture group
181 /// </summary>
182 public CultureGroup()
183 {
184 }
185
186 public CultureGroup(string cultureGroupString)
187 {
188 Debug.Assert(!String.IsNullOrEmpty(cultureGroupString));
189 foreach (var cultureString in cultureGroupString.Split(','))
190 {
191 this.Cultures.Add(cultureString);
192 }
193 }
194
195 public List<string> Cultures { get; } = new List<string>();
196
197 public string OutputFolder
198 {
199 get
200 {
201 if (this.Cultures.Count > 0 &&
202 !this.Cultures[0].Equals(InvariantCultureIdentifier, StringComparison.OrdinalIgnoreCase))
203 {
204 return this.Cultures[0] + "\\";
205 }
206
207 return DefaultFolder;
208 }
209 }
210
211 public string OutputSuffix
212 {
213 get => (this.Cultures.Count > 0) ? this.Cultures[0] : InvariantCultureIdentifier;
214 }
215
216 public override string ToString()
217 {
218 if (this.Cultures.Count > 0)
219 {
220 return String.Join(";", this.Cultures);
221 }
222
223 // We use a keyword for a null culture because MSBuild cannnot handle "" items
224 // Null is different from neutral. For neutral we still want to do WXL
225 // filtering in Light.
226 return NullCultureIdentifier;
227 }
228 }
229 }
230 }