Skip to content

Commit fd5f31c

Browse files
authored
[Xamarin.Android.Build.Tasks] Special Char Support (#6273)
What would we like? The ability for Xamarin.Android projects to reside in directories containing non-ASCII characters. The problem(s)? For starters, many Android SDK utilities don't support non-ASCII characters when running on Windows, e.g.: * https://issuetracker.google.com/issues/188679588 Android Studio (kinda/sorta) "enforces" this, showing a warning message when **Save location** contains non-ASCII characters: ![Android Studio New Project dialog with non-ASCII characters](https://user-images.githubusercontent.com/184788/134550831-b5c5335a-11ca-4296-b30c-cb54e3302917.png) > Save location: /tmp/テスト > … > ⚠️ Your project location contains non-ASCII characters. These are not particularly encouraging foundations. Improve support for non-ASCII characters by updating various parts of our build system to: 1. Prefer *relatives* paths when possible, as we can ensure ASCII-only paths this way, and 2. "Otherwise" change *how* we invoke Android tools to avoid their use of full paths and directory traversal. Additionally, we've long used GNU Binutils to build `libxamarin-app.so` (commit decfbcc, fc3f028, others), and we discovered that Binutils 2.36 introduced [code][0] to deal extra long path names on Windows which makes compilation (in our case using `as`) and linking to break if the source/object files reside in a directory which has non-ASCII characters (in our case they were `テスト`): Xamarin.Android.Common.targets(1887,3): error XA3006: Could not compile native assembly file: typemaps.armeabi-v7a.s [SmokeTestBuildWithSpecialCharactersFalse\テスト\テスト.csproj] [aarch64-linux-android-as.EXE stderr] Assembler messages: (TaskId:187) [aarch64-linux-android-as.EXE stderr] Fatal error: can't create typemaps.arm64-v8a.o: Invalid argument (TaskId:187) Downgrade to Binutils 2.35.2, which doesn't contain the offending code block and will work for us, so long as we use relative paths. It will **still** break with full paths if the path contains special characters, but since our use case involves only relative paths, this is fine. There is also a bug in [`aapt2`][1] which stops it processing a directory which has certain special characters on Windows. This will only appear on projects which have library resources, such as Xamarin.Forms. The problem only appears when processing a directory into a `.flata` archive. Oddly it works fine when processing a file directly. In order to work around this we now generate a resource only `res.zip` file when we extract the library resources. `res.zip` is then used during the generation of the `.flata` archive. Weirdly, this works where directory transversal does not. Additionally it seems to be slightly quicker during incremental builds: * Before 581 ms Aapt2Compile 1 calls * After 366 ms Aapt2Compile 1 calls So the time taken to generate the `res.zip` is offset by the quicker execution of `aapt2 compile`. Finally, Java tooling such as `zipalign` and `apksigner` also failed with the special characters. This was because we were using full paths to the files we were signing/aligning. Switching over to use relative paths fixes this particular problem. Add a set of Unit tests to make sure we can handle the special characters. [0]: https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=bfd/bfdio.c;h=463b3879c52ba6beac47190f8eb0810b0c330e65;hb=HEAD#l124 [1]: https://issuetracker.google.com/issues/188679588
1 parent e244cbf commit fd5f31c

File tree

11 files changed

+113
-21
lines changed

11 files changed

+113
-21
lines changed

build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Xamarin.Android.Prepare
1515
//
1616
partial class Configurables
1717
{
18-
const string BinutilsVersion = "2.37-XA.1";
18+
const string BinutilsVersion = "2.35.2-XA.1";
1919

2020
const string MicrosoftOpenJDK11Version = "11.0.10";
2121
const string MicrosoftOpenJDK11Release = "9.1";

src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Compile.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
using Microsoft.Android.Build.Tasks;
1616

1717
namespace Xamarin.Android.Tasks {
18-
18+
1919
public class Aapt2Compile : Aapt2 {
2020
public override string TaskPrefix => "A2C";
2121

@@ -37,8 +37,8 @@ public class Aapt2Compile : Aapt2 {
3737

3838
protected override int GetRequiredDaemonInstances ()
3939
{
40-
return Math.Min ((ResourcesToCompile ?? ResourceDirectories).Length, DaemonMaxInstanceCount);
41-
}
40+
return Math.Min ((ResourcesToCompile ?? ResourceDirectories)?.Length ?? 1, DaemonMaxInstanceCount);
41+
}
4242

4343
public async override System.Threading.Tasks.Task RunTaskAsync ()
4444
{
@@ -56,6 +56,7 @@ public async override System.Threading.Tasks.Task RunTaskAsync ()
5656
void ProcessDirectory (ITaskItem item, object lockObject)
5757
{
5858
var flatFile = item.GetMetadata ("_FlatFile");
59+
bool isArchive = false;
5960
bool isDirectory = flatFile.EndsWith (".flata", StringComparison.OrdinalIgnoreCase);
6061
if (string.IsNullOrEmpty (flatFile)) {
6162
FileAttributes fa = File.GetAttributes (item.ItemSpec);
@@ -65,9 +66,13 @@ void ProcessDirectory (ITaskItem item, object lockObject)
6566
string fileOrDirectory = item.GetMetadata ("ResourceDirectory");
6667
if (string.IsNullOrEmpty (fileOrDirectory) || !isDirectory)
6768
fileOrDirectory = item.ItemSpec;
69+
if (isDirectory && !Directory.Exists (fileOrDirectory)) {
70+
LogWarning ($"Ignoring directory '{fileOrDirectory}' as it does not exist!");
71+
return;
72+
}
6873
if (isDirectory && !Directory.EnumerateDirectories (fileOrDirectory).Any ())
6974
return;
70-
75+
7176
string outputArchive = isDirectory ? GetFullPath (FlatArchivesDirectory) : GetFullPath (FlatFilesDirectory);
7277
string targetDir = item.GetMetadata ("_ArchiveDirectory");
7378
if (!string.IsNullOrEmpty (targetDir)) {
@@ -83,14 +88,20 @@ void ProcessDirectory (ITaskItem item, object lockObject)
8388
filename = $"{filename}.flata";
8489
outputArchive = Path.Combine (outputArchive, filename);
8590
expectedOutputFile = outputArchive;
91+
string archive = item.GetMetadata (ResolveLibraryProjectImports.ResourceDirectoryArchive);
92+
if (!string.IsNullOrEmpty (archive) && File.Exists (archive)) {
93+
LogDebugMessage ($"Found Compressed Resource Archive '{archive}'.");
94+
fileOrDirectory = archive;
95+
isArchive = true;
96+
}
8697
} else {
8798
if (IsInvalidFilename (fileOrDirectory)) {
8899
LogDebugMessage ($"Invalid filename, ignoring: {fileOrDirectory}");
89100
return;
90101
}
91102
expectedOutputFile = Path.Combine (outputArchive, flatFile);
92103
}
93-
RunAapt (GenerateCommandLineCommands (fileOrDirectory, isDirectory, outputArchive), expectedOutputFile);
104+
RunAapt (GenerateCommandLineCommands (fileOrDirectory, isDirectory, isArchive, outputArchive), expectedOutputFile);
94105
if (isDirectory) {
95106
lock (lockObject)
96107
archives.Add (new TaskItem (expectedOutputFile));
@@ -100,7 +111,7 @@ void ProcessDirectory (ITaskItem item, object lockObject)
100111
}
101112
}
102113

103-
protected string[] GenerateCommandLineCommands (string fileOrDirectory, bool isDirectory, string outputArchive)
114+
protected string[] GenerateCommandLineCommands (string fileOrDirectory, bool isDirectory, bool isArchive, string outputArchive)
104115
{
105116
List<string> cmd = new List<string> ();
106117
cmd.Add ("compile");
@@ -111,7 +122,7 @@ protected string[] GenerateCommandLineCommands (string fileOrDirectory, bool isD
111122
cmd.Add (GetFullPath (ResourceSymbolsTextFile));
112123
}
113124
if (isDirectory) {
114-
cmd.Add ("--dir");
125+
cmd.Add (isArchive ? "--zip" : "--dir");
115126
cmd.Add (GetFullPath (fileOrDirectory).TrimEnd ('\\'));
116127
} else
117128
cmd.Add (GetFullPath (fileOrDirectory));

src/Xamarin.Android.Build.Tasks/Tasks/AndroidApkSigner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ protected override string GenerateCommandLineCommands ()
102102
if (!string.IsNullOrEmpty (AdditionalArguments))
103103
cmd.AppendSwitch (AdditionalArguments);
104104

105-
cmd.AppendSwitchIfNotNull (" ", Path.GetFullPath (ApkToSign));
105+
cmd.AppendSwitchIfNotNull (" ", ApkToSign);
106106

107107
return cmd.ToString ();
108108
}

src/Xamarin.Android.Build.Tasks/Tasks/CollectNonEmptyDirectories.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public override bool RunTask ()
107107
}
108108
var fileTaskItem = new TaskItem (file, new Dictionary<string, string> () {
109109
{ "ResourceDirectory", directory.ItemSpec },
110+
{ ResolveLibraryProjectImports.ResourceDirectoryArchive, directory.GetMetadata (ResolveLibraryProjectImports.ResourceDirectoryArchive) },
110111
{ "StampFile", generateArchive ? stampFile : file },
111112
{ "FilesCache", filesCache},
112113
{ "Hash", stampFile },

src/Xamarin.Android.Build.Tasks/Tasks/JavaToolTask.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ namespace Xamarin.Android.Tasks
1414
public abstract class JavaToolTask : AndroidToolTask
1515
{
1616
/*
17-
Example Javac output for errors. Regex Matches on the first line, we then need to
17+
Example Javac output for errors. Regex Matches on the first line, we then need to
1818
process the second line to get the column number so the IDE can correctly
19-
mark where the error is.
20-
19+
mark where the error is.
20+
2121
TestMe.java:1: error: class, interface, or enum expected
2222
public classo TestMe { }
2323
^
@@ -28,7 +28,7 @@ 2 errors
2828
*/
2929
const string CodeErrorRegExString = @"(?<file>.+\.java):(?<line>\d+):(?<error>.+)";
3030
/*
31-
31+
3232
Sample OutOfMemoryError raised by java. RegEx matches the java.lang.* line of the error
3333
and splits it into an exception and an error
3434
@@ -80,6 +80,8 @@ at com.android.dx.command.Main.main(Main.java:106)
8080

8181
public virtual string DefaultErrorCode => null;
8282

83+
public string WorkingDirectory { get; set; }
84+
8385
protected override string ToolName {
8486
get { return OS.IsWindows ? "java.exe" : "java"; }
8587
}
@@ -100,6 +102,13 @@ protected override bool HandleTaskExecutionErrors ()
100102
return base.HandleTaskExecutionErrors ();
101103
}
102104

105+
protected override string GetWorkingDirectory ()
106+
{
107+
if (!string.IsNullOrEmpty (WorkingDirectory))
108+
return WorkingDirectory;
109+
return base.GetWorkingDirectory ();
110+
}
111+
103112
protected override string GenerateFullPathToTool ()
104113
{
105114
return Path.Combine (ToolPath, ToolExe);
@@ -155,7 +164,7 @@ bool ProcessOutput (string singleLine)
155164
return true;
156165
}
157166
errorText.AppendLine (singleLine);
158-
}
167+
}
159168
return true;
160169
}
161170

@@ -173,7 +182,7 @@ protected override void LogEventsFromTextOutput (string singleLine, MessageImpor
173182
Log.LogMessage (MessageImportance.High, singleLine);
174183
errorLines.Add (singleLine);
175184
return;
176-
}
185+
}
177186
base.LogEventsFromTextOutput (singleLine, messageImportance);
178187
}
179188
}

src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ public class ResolveLibraryProjectImports : AndroidTask
6161

6262
internal const string OriginalFile = "OriginalFile";
6363
internal const string AndroidSkipResourceProcessing = "AndroidSkipResourceProcessing";
64+
65+
internal const string ResourceDirectoryArchive = "ResourceDirectoryArchive";
6466
static readonly string [] knownMetadata = new [] {
6567
OriginalFile,
66-
AndroidSkipResourceProcessing
68+
AndroidSkipResourceProcessing,
69+
ResourceDirectoryArchive
6770
};
6871

6972
AssemblyIdentityMap assemblyMap = new AssemblyIdentityMap();
@@ -177,6 +180,7 @@ void Extract (
177180
string importsDir = Path.Combine (outDirForDll, ImportsDirectory);
178181
string nativeimportsDir = Path.Combine (outDirForDll, NativeImportsDirectory);
179182
string resDir = Path.Combine (importsDir, "res");
183+
string resDirArchive = Path.Combine (resDir, "..", "res.zip");
180184
string assetsDir = Path.Combine (importsDir, "assets");
181185

182186
// Skip already-extracted resources.
@@ -194,6 +198,7 @@ void Extract (
194198
if (Directory.Exists (resDir)) {
195199
var taskItem = new TaskItem (Path.GetFullPath (resDir), new Dictionary<string, string> {
196200
{ OriginalFile, assemblyPath },
201+
{ ResourceDirectoryArchive, Path.GetFullPath (resDirArchive)},
197202
});
198203
if (bool.TryParse (assemblyItem.GetMetadata (AndroidSkipResourceProcessing), out skip) && skip)
199204
taskItem.SetMetadata (AndroidSkipResourceProcessing, "True");
@@ -290,8 +295,11 @@ void Extract (
290295
// which resulted in missing resource issue.
291296
// Here we replaced copy with use of '-S' option and made it to work.
292297
if (Directory.Exists (resDir)) {
298+
CreateResourceArchive (resDir, resDirArchive);
293299
var taskItem = new TaskItem (Path.GetFullPath (resDir), new Dictionary<string, string> {
294-
{ OriginalFile, assemblyPath }
300+
{ OriginalFile, assemblyPath },
301+
{ ResourceDirectoryArchive, Path.GetFullPath (resDirArchive)},
302+
295303
});
296304
if (bool.TryParse (assemblyItem.GetMetadata (AndroidSkipResourceProcessing), out skip) && skip)
297305
taskItem.SetMetadata (AndroidSkipResourceProcessing, "True");
@@ -335,6 +343,7 @@ void Extract (
335343
string outDirForDll = Path.Combine (OutputImportDirectory, aarIdentityName);
336344
string importsDir = Path.Combine (outDirForDll, ImportsDirectory);
337345
string resDir = Path.Combine (importsDir, "res");
346+
string resDirArchive = Path.Combine (resDir, "..", "res.zip");
338347
string assetsDir = Path.Combine (importsDir, "assets");
339348

340349
bool updated = false;
@@ -357,6 +366,7 @@ void Extract (
357366
resolvedResourceDirectories.Add (new TaskItem (Path.GetFullPath (resDir), new Dictionary<string, string> {
358367
{ OriginalFile, Path.GetFullPath (aarFile.ItemSpec) },
359368
{ AndroidSkipResourceProcessing, skipProcessing },
369+
{ ResourceDirectoryArchive, Path.GetFullPath (resDirArchive)},
360370
}));
361371
}
362372
if (Directory.Exists (assetsDir))
@@ -406,13 +416,15 @@ void Extract (
406416
}
407417
}
408418
if (Directory.Exists (resDir)) {
419+
CreateResourceArchive (resDir, resDirArchive);
409420
var skipProcessing = aarFile.GetMetadata (AndroidSkipResourceProcessing);
410421
if (string.IsNullOrEmpty (skipProcessing)) {
411422
skipProcessing = "True";
412423
}
413424
resolvedResourceDirectories.Add (new TaskItem (Path.GetFullPath (resDir), new Dictionary<string, string> {
414425
{ OriginalFile, aarFullPath },
415426
{ AndroidSkipResourceProcessing, skipProcessing },
427+
{ ResourceDirectoryArchive, Path.GetFullPath (resDirArchive)},
416428
}));
417429
}
418430
if (Directory.Exists (assetsDir))
@@ -422,6 +434,16 @@ void Extract (
422434
}
423435
}
424436

437+
void CreateResourceArchive (string resDir, string outputFile)
438+
{
439+
var fileMode = File.Exists (outputFile) ? FileMode.Open : FileMode.CreateNew;
440+
Files.ArchiveZipUpdate (outputFile, f => {
441+
using (var zip = new ZipArchiveEx (f, fileMode)) {
442+
zip.AddDirectory (resDir, "res");
443+
}
444+
});
445+
}
446+
425447
static void AddJar (IDictionary<string, ITaskItem> jars, string destination, string path, string originalFile = null)
426448
{
427449
var fullPath = Path.GetFullPath (Path.Combine (destination, path));

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ public string GetFoo () {
830830
rawToDelete,
831831
},
832832
};
833+
libProj.SetProperty ("Deterministic", "true");
833834
var appProj = new XamarinAndroidApplicationProject () {
834835
IsRelease = true,
835836
ProjectName = "App1",

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ namespace Xamarin.Android.Build.Tests
2323
[Parallelizable (ParallelScope.Children)]
2424
public partial class BuildTest : BaseTest
2525
{
26+
[Test]
27+
[Category ("SmokeTests")]
28+
public void SmokeTestBuildWithSpecialCharacters ([Values (false, true)] bool forms)
29+
{
30+
var testName = "テスト";
31+
32+
var rootPath = Path.Combine (Root, "temp", TestName);
33+
var proj = forms ?
34+
new XamarinFormsAndroidApplicationProject () :
35+
new XamarinAndroidApplicationProject ();
36+
proj.ProjectName = testName;
37+
proj.IsRelease = true;
38+
if (forms) {
39+
proj.PackageReferences.Clear ();
40+
proj.PackageReferences.Add (KnownPackages.XamarinForms_4_7_0_1142);
41+
}
42+
using (var builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName))){
43+
Assert.IsTrue (builder.Build (proj), "Build should have succeeded.");
44+
}
45+
}
46+
2647
public static string GetLinkedPath (ProjectBuilder builder, bool isRelease, string filename)
2748
{
2849
return Builder.UseDotNet && isRelease ?

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,7 +2237,7 @@ because xbuild doesn't support framework reference assemblies.
22372237
<_JarSignerSuffix Condition=" '$(AndroidPackageFormat)' == 'aab' ">-Signed</_JarSignerSuffix>
22382238
</PropertyGroup>
22392239
<AndroidSignPackage Condition=" '$(AndroidUseApkSigner)' != 'true' "
2240-
UnsignedApk="%(ApkAbiFilesIntermediate.FullPath)"
2240+
UnsignedApk="%(ApkAbiFilesIntermediate.Identity)"
22412241
SignedApkDirectory="$(OutDir)"
22422242
FileSuffix="$(_JarSignerSuffix)"
22432243
KeyStore="$(_ApkKeyStore)"
@@ -2257,7 +2257,7 @@ because xbuild doesn't support framework reference assemblies.
22572257
</ItemGroup>
22582258
<Delete Files="%(ApkAbiFilesSigned.FullPath)" Condition=" '$(AndroidUseApkSigner)' == 'true' "/>
22592259
<AndroidZipAlign Condition=" '$(AndroidUseApkSigner)' == 'true' "
2260-
Source="%(ApkAbiFilesIntermediate.FullPath)"
2260+
Source="%(ApkAbiFilesIntermediate.Identity)"
22612261
DestinationDirectory="$(OutDir)"
22622262
ToolPath="$(ZipAlignToolPath)"
22632263
ToolExe="$(ZipalignToolExe)"
@@ -2268,7 +2268,7 @@ because xbuild doesn't support framework reference assemblies.
22682268
</ItemGroup>
22692269
<AndroidApkSigner Condition=" '$(AndroidUseApkSigner)' == 'true' "
22702270
ApkSignerJar="$(ApkSignerJar)"
2271-
ApkToSign="%(ApkAbiFilesAligned.FullPath)"
2271+
ApkToSign="%(ApkAbiFilesAligned.Identity)"
22722272
KeyStore="$(_ApkKeyStore)"
22732273
KeyAlias="$(_ApkKeyAlias)"
22742274
KeyPass="$(_ApkKeyPass)"
@@ -2292,7 +2292,7 @@ because xbuild doesn't support framework reference assemblies.
22922292
</ItemGroup>
22932293
<Message Text="Unaligned android package '%(ApkAbiFilesUnaligned.FullPath)'" Condition=" '$(AndroidUseApkSigner)' != 'True' And '$(AndroidPackageFormat)' != 'aab' "/>
22942294
<AndroidZipAlign Condition=" '$(AndroidUseApkSigner)' != 'True' And '$(AndroidPackageFormat)' != 'aab' "
2295-
Source="%(ApkAbiFilesUnaligned.FullPath)"
2295+
Source="%(ApkAbiFilesUnaligned.Identity)"
22962296
DestinationDirectory="$(OutDir)"
22972297
ToolPath="$(ZipAlignToolPath)"
22982298
ToolExe="$(ZipalignToolExe)"

src/Xamarin.Android.Build.Tasks/Xamarin.Android.EmbeddedResource.targets

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ This file is used by all project types, including binding projects.
5656
<Output TaskParameter="ResolvedEnvironmentFiles" ItemName="LibraryEnvironments" />
5757
<Output TaskParameter="ResolvedResourceDirectoryStamps" ItemName="_LibraryResourceDirectoryStamps" />
5858
</ReadLibraryProjectImportsCache>
59+
<ItemGroup>
60+
<FileWrites Include="@(ResolvedResourceDirectories->'%(ResourceDirectoryArchive)')"
61+
Condition=" '%(ResolvedResourceDirectories.ResourceDirectoryArchive)' != '' And Exists ('%(ResolvedResourceDirectories.ResourceDirectoryArchive)')" />
62+
</ItemGroup>
5963
</Target>
6064

6165
<Target Name="_BuildLibraryImportsCache"

0 commit comments

Comments
 (0)