同时支持 armv7 和 arm64 的链接库

在开发 Android 的时候,有需求需要同时支持 arm64 和 armv7,需要在 Build.cs 中同时把 armv7 和 arm64 的链接库都添加到 PublicAdditionalLibraries 中:

1
2
3
4
5
PublicAdditionalLibraries.AddRange(new string[]
{
Path.Combine(ThirdPartyFolder, "Android_armeabi-v7a", AkConfigurationDir),
Path.Combine(ThirdPartyFolder, "Android_arm64-v8a", AkConfigurationDir),
});

但是,在 UE 的 ModuleRules 里没有能判断当前编译的架构的方法(ModuleRules 的构造在编译时只会执行一次),导致编译 arm64 的时候找到了 armv7 的链接库,导致链接错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
14>ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux
ld.lld: error: too many errors emitted, stopping now (use -error-limit=0 to see all errors)
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
14>Execution failed. Error: 1 (0x01) Target: 'C:\BuildAgent\workspace\FGameClientBuild\Client\Binaries\Android\FGame-arm64.so'

所以,需要找到一种方法,能够在使用 PublicAdditionalLibraries 同时添加了 arm64 和 armv7 链接库的情况下让编译器能够自动地匹配到应该去什么路径来执行链接。

翻了一下 UBT 的代码,发现 UE 中对在 Android 上对链接库的路径做了模式匹配:

Engine\Source\Programs\UnrealBuildTool\Platform\Android\AndroidToolChain.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static private Dictionary<string, string[]> AllArchNames = new Dictionary<string, string[]> {
{ "-armv7", new string[] { "armv7", "armeabi-v7a", } },
{ "-arm64", new string[] { "arm64", "arm64-v8a", } },
{ "-x86", new string[] { "x86", } },
{ "-x64", new string[] { "x64", "x86_64", } },
};

static bool IsDirectoryForArch(string Dir, string Arch)
{
// make sure paths use one particular slash
Dir = Dir.Replace("\\", "/").ToLowerInvariant();

// look for other architectures in the Dir path, and fail if it finds it
foreach (KeyValuePair<string, string[]> Pair in AllArchNames)
{
if (Pair.Key != Arch)
{
foreach (string ArchName in Pair.Value)
{
// if there's a directory in the path with a bad architecture name, reject it
if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase))
{
return false;
}
}
}
}

// if nothing was found, we are okay
return true;
}

public override FileItem[] LinkAllFiles(LinkEnvironment LinkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph)
{
// ...
// Add the library paths to the additional path list
foreach (DirectoryReference LibraryPath in LinkEnvironment.LibraryPaths)
{
// LinkerPaths could be relative or absolute
string AbsoluteLibraryPath = Utils.ExpandVariables(LibraryPath.FullName);
if (IsDirectoryForArch(AbsoluteLibraryPath, Arch))
{
// environment variables aren't expanded when using the $(style
if (Path.IsPathRooted(AbsoluteLibraryPath) == false)
{
AbsoluteLibraryPath = Path.Combine(LinkerPath.FullName, AbsoluteLibraryPath);
}
AbsoluteLibraryPath = Utils.CollapseRelativeDirectories(AbsoluteLibraryPath);
if (!AdditionalLibraryPaths.Contains(AbsoluteLibraryPath))
{
AdditionalLibraryPaths.Add(AbsoluteLibraryPath);
}
}
}

// ...
}

最关键的就是这这一行:

1
if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase))

根据上面正则的规则,可以把链接库的路径改为:

1
2
XXXX/armeabi-v7a/
XXXX/armeabi-v8a/

只要我们为 Android 添加的链接库路径匹配这个规则,使用 UE 编译时就会自动使用对应架构的链接库(.a 和.so 都是可以使用这个规则的)。

UE 这也太坑了,这个要求文档没写,完全是一个潜规则。