Files
DeviantMobile-Rohan/Assets/GooglePlayPlugins/com.google.android.appbundle/Editor/Scripts/Internal/BuildTools/AppBundleBuilder.cs

992 lines
44 KiB
C#

// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Xml.Linq;
using Google.Android.AppBundle.Editor.Internal.AndroidManifest;
using Google.Android.AppBundle.Editor.Internal.AssetPacks;
using Google.Android.AppBundle.Editor.Internal.Config;
using Google.Android.AppBundle.Editor.Internal.Utils;
using UnityEditor;
using UnityEngine;
#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
using System.Threading.Tasks;
#endif
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
{
/// <summary>
/// Helper to build an Android App Bundle file on Unity.
/// </summary>
public class AppBundleBuilder : IBuildTool
{
public delegate void PostBuildCallback(string finishedAabFilePath);
/// <summary>
/// Status for coordinating between the background thread and main thread on async builds.
/// </summary>
private enum BuildStatus
{
Running,
Failing,
Succeeding,
Halted
}
private const string AndroidManifestFileName = "AndroidManifest.xml";
private const string AssetsDirectoryName = "assets";
private const string BundleMetadataDirectoryName = "BUNDLE-METADATA";
private const string ManifestDirectoryName = "manifest";
private const string RequiredDirectoryName = "assets/bin/Data/Managed";
private const string ResourceTableFileName = "resources.pb";
private const float ProgressCreateBaseModule = 0.3f;
private const float ProgressProcessModules = 0.5f;
private const float ProgressRunBundletool = 0.7f;
private const int ProgressBarWaitHandleTimeoutMs = 100;
// For simplicity we use the same filename when building an APK or an AAB.
// Note: AAB builds fail if we use a .apk file extension, but APK builds can use any extension.
private const string AndroidPlayerFileName = "tmp.aab";
private const string AndroidPlayerFilePrefix = "tmp";
/// <summary>
/// The folder where to store the asset pack, inside the "assets" folder of an Android App Bundle module.
/// This intermediate folder name can be suffixed with a texture compression format targeting (e.g: #tcf_astc),
/// which will be stripped out by bundletool.
/// </summary>
private const string AssetPackFolder = "assetpack";
private readonly AndroidAssetPackagingTool _androidAssetPackagingTool;
private readonly AndroidBuilder _androidBuilder;
private readonly BundletoolHelper _bundletool;
private readonly JarSigner _jarSigner;
private readonly string _workingDirectoryPath;
private readonly ZipUtils _zipUtils;
private volatile BuildStatus _buildStatus = BuildStatus.Running;
private volatile string _progressBarMessage = "";
private volatile float _progressBarProgress;
private volatile string _buildErrorMessage;
private volatile string _finishedAabFilePath;
private volatile bool _canceled;
private Thread _backgroundThread;
private EventWaitHandle _progressBarWaitHandle;
private AndroidSdkVersions _minSdkVersion;
private string _packageName;
private int _versionCode;
private string _versionName;
private PostBuildCallback _createBundleAsyncOnSuccess = delegate { };
private IEnumerable<IAssetPackManifestTransformer> _assetPackManifestTransformers;
/// <summary>
/// Constructor.
/// </summary>
public AppBundleBuilder(
AndroidAssetPackagingTool androidAssetPackagingTool,
AndroidBuilder androidBuilder,
BundletoolHelper bundletool,
JarSigner jarSigner,
string workingDirectoryPath,
ZipUtils zipUtils)
{
_androidAssetPackagingTool = androidAssetPackagingTool;
_androidBuilder = androidBuilder;
_bundletool = bundletool;
_jarSigner = jarSigner;
_workingDirectoryPath = workingDirectoryPath;
_zipUtils = zipUtils;
}
public virtual bool Initialize(BuildToolLogger buildToolLogger)
{
// Cache information that is only accessible from the main thread.
_minSdkVersion = PlayerSettings.Android.minSdkVersion;
_packageName = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android);
_versionCode = PlayerSettings.Android.bundleVersionCode;
_versionName = PlayerSettings.bundleVersion;
_assetPackManifestTransformers = AssetPackManifestTransformerRegistry.Registry.ConstructInstances();
var initializedManifestTransformers = true;
foreach (var transformer in _assetPackManifestTransformers)
{
initializedManifestTransformers &= transformer.Initialize(buildToolLogger);
}
return CheckUnityVersion(buildToolLogger)
&& initializedManifestTransformers
&& _androidAssetPackagingTool.Initialize(buildToolLogger)
&& _androidBuilder.Initialize(buildToolLogger)
&& _jarSigner.Initialize(buildToolLogger)
&& _bundletool.Initialize(buildToolLogger)
&& _zipUtils.Initialize(buildToolLogger);
}
public string WorkingDirectoryPath
{
get { return _workingDirectoryPath; }
}
private string AndroidPlayerFilePath
{
get { return Path.Combine(_workingDirectoryPath, AndroidPlayerFileName); }
}
/// <summary>
/// Builds an Android Player with the specified options.
/// Note: the specified <see cref="BuildPlayerOptions.locationPathName"/> field is ignored and the
/// Android Player is written to a temporary file.
/// </summary>
public virtual AndroidBuildResult BuildAndroidPlayer(BuildPlayerOptions buildPlayerOptions)
{
var workingDirectory = new DirectoryInfo(_workingDirectoryPath);
if (workingDirectory.Exists)
{
workingDirectory.Delete(true);
}
workingDirectory.Create();
EditorUserBuildSettings.buildAppBundle = true;
Debug.LogFormat("Building Android Player: {0}", AndroidPlayerFilePath);
// This Android Player is an intermediate build artifact, so use a temporary path for the output file path.
var updatedBuildPlayerOptions = new BuildPlayerOptions
{
assetBundleManifestPath = buildPlayerOptions.assetBundleManifestPath,
locationPathName = AndroidPlayerFilePath,
options = buildPlayerOptions.options,
scenes = buildPlayerOptions.scenes,
target = buildPlayerOptions.target,
targetGroup = buildPlayerOptions.targetGroup
};
// Do not use BuildAndSign since this signature won't be used.
return _androidBuilder.Build(updatedBuildPlayerOptions);
}
/// <summary>
/// Synchronously builds an AAB given the specified options and existing Android Player on disk.
/// </summary>
/// <returns>An error message if there was an error, or null if successful.</returns>
public string CreateBundle(CreateBundleOptions options)
{
if (_buildStatus != BuildStatus.Running)
{
throw new Exception("Unexpected call to CreateBundle() with status: " + _buildStatus);
}
var moduleDirectoryList = new List<DirectoryInfo>();
var workingDirectory = new DirectoryInfo(_workingDirectoryPath);
var error = CreateAssetModules(moduleDirectoryList, options.AssetPackConfig, workingDirectory);
if (error != null)
{
return error;
}
IList<string> bundleMetadata;
error = CreateBaseModules(moduleDirectoryList, options, workingDirectory, out bundleMetadata);
if (error != null)
{
return error;
}
error = CreateBundle(moduleDirectoryList, options, bundleMetadata);
if (error != null)
{
return error;
}
Debug.LogFormat("Finished building app bundle: {0}", options.AabFilePath);
_finishedAabFilePath = options.AabFilePath;
_buildStatus = BuildStatus.Succeeding;
return null;
}
/// <summary>
/// Asynchronously builds an AAB at the specified path.
/// </summary>
/// <param name="aabFilePath">The AAB output file path.</param>
/// <param name="assetPackConfig">Asset packs to include in the AAB.</param>
/// <param name="onSuccess">
/// Callback that fires with the final aab file location, when the bundle creation succeeds.
/// </param>
public void CreateBundleAsync(CreateBundleOptions options, PostBuildCallback onSuccess)
{
// Copy the AssetPackConfig before leaving the main thread in case the original is modified later.
options.AssetPackConfig = SerializationHelper.DeepCopy(options.AssetPackConfig);
_createBundleAsyncOnSuccess = onSuccess;
StartCreateBundleAsync(() =>
{
try
{
CreateBundle(options);
}
catch (ThreadAbortException ex)
{
if (!_canceled)
{
// Unexpected ThreadAbortException.
DisplayBuildError("Exception", ex.ToString());
}
}
catch (Exception ex)
{
// Catch and display exceptions since they may otherwise be undetected on a background thread.
DisplayBuildError("Exception", ex.ToString());
}
});
}
#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
/// <summary>
/// Synchronously builds an Android Player and then produces a final AAB synchronously or asynchronously,
/// as specified.
/// </summary>
/// <param name="androidBuildOptions">Options indicating how to build the AAB, including asset packs.</param>
/// <returns>An async task that provides an AndroidBuildReport.</returns>
public async Task<AndroidBuildReport> CreateBundleWithTask(AndroidBuildOptions androidBuildOptions)
{
var taskCompletionSource = new TaskCompletionSource<AndroidBuildReport>();
var androidBuildResult = BuildAndroidPlayer(androidBuildOptions.BuildPlayerOptions);
if (androidBuildResult.Cancelled)
{
taskCompletionSource.SetCanceled();
return await taskCompletionSource.Task;
}
var androidBuildReport = new AndroidBuildReport(androidBuildResult.Report);
if (androidBuildResult.ErrorMessage != null)
{
taskCompletionSource.SetException(
new AndroidBuildException(androidBuildResult.ErrorMessage, androidBuildReport));
return await taskCompletionSource.Task;
}
var createBundleOptions = new CreateBundleOptions
{
AabFilePath = androidBuildOptions.BuildPlayerOptions.locationPathName,
AssetPackConfig = androidBuildOptions.AssetPackConfig ?? new AssetPackConfig(),
CompressionOptions = androidBuildOptions.CompressionOptions
};
if (androidBuildOptions.ForceSingleThreadedBuild || Application.isBatchMode)
{
CreateBundleInternal(
taskCompletionSource,
() => CreateBundle(createBundleOptions),
androidBuildReport,
androidBuildReport);
}
else
{
// Copy the AssetPackConfig while still on the main thread in case the original is modified later.
createBundleOptions.AssetPackConfig = SerializationHelper.DeepCopy(createBundleOptions.AssetPackConfig);
StartCreateBundleAsync(() =>
{
CreateBundleInternal(
taskCompletionSource,
() => CreateBundle(createBundleOptions),
androidBuildReport,
androidBuildReport);
});
}
return await taskCompletionSource.Task;
}
/// <summary>
/// Builds an Android App Bundle containing only asset packs.
/// </summary>
public async Task CreateAssetOnlyBundle(AssetOnlyBuildOptions assetOnlyBuildOptions)
{
var assetOnlyOptions = new AssetOnlyOptions
{
AppVersions = new List<long>(assetOnlyBuildOptions.AppVersions),
AssetVersionTag = assetOnlyBuildOptions.AssetVersionTag
};
var createBundleOptions = new CreateBundleOptions
{
AabFilePath = assetOnlyBuildOptions.LocationPathName,
AssetPackConfig = SerializationHelper.DeepCopy(assetOnlyBuildOptions.AssetPackConfig),
AssetOnlyOptions = assetOnlyOptions
};
var completionSource = new TaskCompletionSource<bool>();
if (assetOnlyBuildOptions.ForceSingleThreadedBuild || Application.isBatchMode)
{
CreateBundleInternal(
completionSource,
() => CreateAssetOnlyBundle(createBundleOptions),
true);
}
else
{
StartCreateBundleAsync(() =>
{
CreateBundleInternal(
completionSource,
() => CreateAssetOnlyBundle(createBundleOptions),
true);
});
}
await completionSource.Task;
}
private string CreateAssetOnlyBundle(CreateBundleOptions options)
{
if (_buildStatus != BuildStatus.Running)
{
throw new Exception("Unexpected call to CreateAssetOnlyBundle() with status: " + _buildStatus);
}
var moduleDirectoryList = new List<DirectoryInfo>();
var workingDirectory = new DirectoryInfo(_workingDirectoryPath);
var error = CreateAssetModules(moduleDirectoryList, options.AssetPackConfig, workingDirectory);
if (error != null)
{
return error;
}
error = CreateBundle(moduleDirectoryList, options);
if (error != null)
{
return error;
}
Debug.LogFormat("Finished building asset-only app bundle: {0}", options.AabFilePath);
_finishedAabFilePath = options.AabFilePath;
_buildStatus = BuildStatus.Succeeding;
return null;
}
private void CreateBundleInternal<T>(
TaskCompletionSource<T> taskCompletionSource,
Func<string> createBundleFunc,
T successResult,
AndroidBuildReport androidBuildReport = null)
{
try
{
var errorMessage = createBundleFunc.Invoke();
if (errorMessage == null)
{
taskCompletionSource.SetResult(successResult);
}
else
{
// Already logged.
taskCompletionSource.SetException(new AndroidBuildException(errorMessage, androidBuildReport));
}
}
catch (ThreadAbortException ex)
{
if (_canceled)
{
taskCompletionSource.SetCanceled();
}
else
{
// Unexpected ThreadAbortException.
taskCompletionSource.SetException(new AndroidBuildException(ex, androidBuildReport));
DisplayBuildError("Exception", ex.ToString());
}
}
catch (Exception ex)
{
taskCompletionSource.SetException(new AndroidBuildException(ex, androidBuildReport));
DisplayBuildError("Exception", ex.ToString());
}
}
#endif
private string CreateBaseModules(IList<DirectoryInfo> moduleDirectoryList, CreateBundleOptions options,
DirectoryInfo workingDirectory, out IList<string> bundleMetadata)
{
// Create base module directory.
bundleMetadata = new List<string>();
var baseDirectory = workingDirectory.CreateSubdirectory(AndroidAppBundle.BaseModuleName);
var baseErrorMessage = CreateBaseModule(baseDirectory, out bundleMetadata);
if (baseErrorMessage != null)
{
// Already displayed the error.
return baseErrorMessage;
}
moduleDirectoryList.Add(baseDirectory);
if (options.AssetPackConfig.SplitBaseModuleAssets)
{
// Move assets from base module directory to the separate module's directory.
var splitBaseDirectory = workingDirectory.CreateSubdirectory(AndroidAppBundle.BaseAssetsModuleName);
var splitBaseErrorMessage = CreateSplitBaseModule(baseDirectory, splitBaseDirectory);
if (splitBaseErrorMessage != null)
{
// Already displayed the error.
return splitBaseErrorMessage;
}
moduleDirectoryList.Add(splitBaseDirectory);
}
return null;
}
private string CreateAssetModules(IList<DirectoryInfo> moduleDirectoryList, AssetPackConfig assetPackConfig,
DirectoryInfo workingDirectory)
{
// Create asset pack module directories.
var index = 0;
var assetPacks = assetPackConfig.DeliveredAssetPacks;
foreach (var entry in assetPacks)
{
DisplayProgress(
string.Format("Processing asset pack {0} of {1}", index + 1, assetPacks.Count),
Mathf.Lerp(0.1f, ProgressCreateBaseModule, (float)index / assetPacks.Count));
index++;
var assetPackName = entry.Key;
var assetPack = entry.Value;
var assetPackDirectoryInfo = workingDirectory.CreateSubdirectory(assetPackName);
var assetPackErrorMessage = CreateAssetPackModule(assetPackName, assetPack, assetPackDirectoryInfo);
if (assetPackErrorMessage != null)
{
// Already displayed the error.
return assetPackErrorMessage;
}
moduleDirectoryList.Add(assetPackDirectoryInfo);
}
return null;
}
private string CreateBundle(List<DirectoryInfo> moduleDirectoryList, CreateBundleOptions options,
IList<string> bundleMetadata = null)
{
// Create a ZIP file for each module directory.
var moduleFiles = new List<string>();
var numModules = moduleDirectoryList.Count;
for (var i = 0; i < numModules; i++)
{
if (numModules == 1)
{
DisplayProgress("Processing base module", ProgressProcessModules);
}
else
{
DisplayProgress(
string.Format("Processing module {0} of {1}", i + 1, numModules),
Mathf.Lerp(ProgressProcessModules, ProgressRunBundletool, (float)i / numModules));
}
var moduleDirectoryInfo = moduleDirectoryList[i];
var destinationDirectoryInfo = GetDestinationSubdirectory(moduleDirectoryInfo);
// Create ZIP file path, for example /path/to/files/base becomes /path/to/files/base/base.zip
var zipFilePath = Path.Combine(moduleDirectoryInfo.FullName, moduleDirectoryInfo.Name + ".zip");
var zipErrorMessage = _zipUtils.CreateZipFile(zipFilePath, destinationDirectoryInfo.FullName, ".");
if (zipErrorMessage != null)
{
return DisplayBuildError("Zip creation", zipErrorMessage);
}
moduleFiles.Add(zipFilePath);
}
if (bundleMetadata == null)
{
bundleMetadata = new List<string>();
}
DisplayProgress("Running bundletool", ProgressRunBundletool);
var configParams = CreateBuildBundleConfigParams(options);
var buildBundleErrorMessage =
_bundletool.BuildBundle(options.AabFilePath, moduleFiles, bundleMetadata, configParams);
if (buildBundleErrorMessage != null)
{
return DisplayBuildError("Bundletool", buildBundleErrorMessage);
}
// Only sign the .aab if a custom keystore is configured.
if (_jarSigner.UseCustomKeystore)
{
DisplayProgress("Signing bundle", 0.9f);
var signingErrorMessage = _jarSigner.Sign(options.AabFilePath);
if (signingErrorMessage != null)
{
Debug.LogError("Failed to sign");
return DisplayBuildError("Signing", signingErrorMessage);
}
}
else
{
Debug.LogFormat("Skipped signing since a Custom Keystore isn't configured in Android Player Settings");
}
MoveSymbolsZipFile(options.AabFilePath);
return null;
}
private BundletoolHelper.BuildBundleConfigParams CreateBuildBundleConfigParams(CreateBundleOptions options)
{
var configParams = new BundletoolHelper.BuildBundleConfigParams
{
defaultTcfSuffix = TextureTargetingTools.GetBundleToolTextureCompressionFormatName(
options.AssetPackConfig.DefaultTextureCompressionFormat),
defaultDeviceTier =
DeviceTierTargetingTools.GetBundleToolDeviceTierFormatName(
options.AssetPackConfig.DefaultDeviceTier),
minSdkVersion = _minSdkVersion,
compressionOptions = options.CompressionOptions ?? new CompressionOptions(),
containsInstallTimeAssetPack = options.AssetPackConfig.SplitBaseModuleAssets
};
var assetPacks = options.AssetPackConfig.DeliveredAssetPacks;
foreach (var entry in assetPacks)
{
var assetPackName = entry.Key;
var assetPack = entry.Value;
configParams.enableTcfTargeting |= assetPack.CompressionFormatToAssetBundleFilePath != null;
configParams.enableTcfTargeting |= assetPack.CompressionFormatToAssetPackDirectoryPath != null;
configParams.enableDeviceTierTargeting |= assetPack.DeviceTierToAssetBundleFilePath != null;
configParams.enableDeviceTierTargeting |= assetPack.DeviceTierToAssetPackDirectoryPath != null;
configParams.containsInstallTimeAssetPack |=
assetPack.DeliveryMode == AssetPackDeliveryMode.InstallTime;
}
configParams.containsInstallTimeAssetPack |= options.AssetPackConfig.SplitBaseModuleAssets;
configParams.assetOnlyOptions = options.AssetOnlyOptions;
return configParams;
}
private void StartCreateBundleAsync(ThreadStart threadStart)
{
_progressBarWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
EditorApplication.update += HandleUpdate;
_backgroundThread = new Thread(threadStart);
_backgroundThread.Name = "AppBundle";
_backgroundThread.Start();
}
// Handler for EditorApplication.update callbacks on the main thread.
private void HandleUpdate()
{
switch (_buildStatus)
{
case BuildStatus.Running:
// Checking for task cancellation during EditorApplication.update is much more responsive
// than only calling DisplayCancelableProgressBar() when the message changes.
if (EditorUtility.DisplayCancelableProgressBar(
"Building App Bundle", _progressBarMessage, _progressBarProgress))
{
Debug.Log("Cancelling app bundle build...");
EditorUtility.ClearProgressBar();
_canceled = true;
_backgroundThread.Abort();
_buildStatus = BuildStatus.Halted;
}
// Signal the background thread that we've handled an update since DisplayProgress() was called.
_progressBarWaitHandle.Set();
break;
case BuildStatus.Failing:
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog(
BuildToolLogger.BuildErrorTitle, _buildErrorMessage, WindowUtils.OkButtonText);
_buildStatus = BuildStatus.Halted;
break;
case BuildStatus.Succeeding:
EditorUtility.ClearProgressBar();
_buildStatus = BuildStatus.Halted;
_createBundleAsyncOnSuccess(_finishedAabFilePath);
break;
case BuildStatus.Halted:
_progressBarWaitHandle.Close();
EditorApplication.update -= HandleUpdate;
break;
default:
throw new Exception("Unexpected BuildStatus: " + _buildStatus);
}
}
private string CreateAssetPackModule(
string assetPackName, AssetPack assetPack, DirectoryInfo assetPackDirectoryInfo)
{
var aaptErrorMessage =
CreateAssetPackWithManifest(assetPackDirectoryInfo, assetPackName, assetPack.DeliveryMode);
if (aaptErrorMessage != null)
{
// Already displayed the error.
return aaptErrorMessage;
}
// Copy all assets to an "assets" subdirectory in the destination folder.
var destinationAssetsDirectory =
GetDestinationSubdirectory(assetPackDirectoryInfo).CreateSubdirectory(AssetsDirectoryName);
if (assetPack.AssetBundleFilePath != null)
{
// Copy AssetBundle files into the module's "assets" folder, inside an "assetpack" folder.
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(AssetPackFolder).FullName;
File.Copy(assetPack.AssetBundleFilePath, Path.Combine(outputDirectory, assetPackName));
}
else if (assetPack.CompressionFormatToAssetBundleFilePath != null)
{
// Copy the AssetBundle files into the module's "assets" folder, inside an "assetpack#tcf_xxx" folder.
foreach (var compressionAndFilePath in assetPack.CompressionFormatToAssetBundleFilePath)
{
var targetedAssetsFolderName =
AssetPackFolder + TextureTargetingTools.GetTargetingSuffix(compressionAndFilePath.Key);
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
File.Copy(compressionAndFilePath.Value, Path.Combine(outputDirectory.FullName, assetPackName));
}
}
else if (assetPack.DeviceTierToAssetBundleFilePath != null)
{
// Copy the AssetBundle files into the module's "assets" folder, inside an "assetpack#tier_xxx" folder.
foreach (var deviceTierAndFilePath in assetPack.DeviceTierToAssetBundleFilePath)
{
var targetedAssetsFolderName =
AssetPackFolder + DeviceTierTargetingTools.GetTargetingSuffix(deviceTierAndFilePath.Key);
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
File.Copy(deviceTierAndFilePath.Value, Path.Combine(outputDirectory.FullName, assetPackName));
}
}
else if (assetPack.AssetPackDirectoryPath != null)
{
var sourceAssetsDirectory = new DirectoryInfo(assetPack.AssetPackDirectoryPath);
if (!sourceAssetsDirectory.Exists)
{
// TODO: check this earlier.
return DisplayBuildError("Missing directory for " + assetPackName, sourceAssetsDirectory.FullName);
}
// Copy asset pack files into the module's "assets" folder, inside an "assetpack" folder.
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(AssetPackFolder);
CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
}
else if (assetPack.CompressionFormatToAssetPackDirectoryPath != null)
{
// Copy asset pack files into the module's "assets" folder, inside an "assetpack#tcf_xxx" folder.
foreach (var compressionAndDirectoryPath in assetPack.CompressionFormatToAssetPackDirectoryPath)
{
var sourceAssetsDirectory = new DirectoryInfo(compressionAndDirectoryPath.Value);
if (!sourceAssetsDirectory.Exists)
{
// TODO: check this earlier.
return DisplayBuildError(
"Missing directory for " + assetPackName, sourceAssetsDirectory.FullName);
}
var targetedAssetsFolderName =
AssetPackFolder + TextureTargetingTools.GetTargetingSuffix(compressionAndDirectoryPath.Key);
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
}
}
else if (assetPack.DeviceTierToAssetPackDirectoryPath != null)
{
// Copy asset pack files into the module's "assets" folder, inside an "assetpack#tier_xxx" folder.
foreach (var deviceTierAndDirectoryPath in assetPack.DeviceTierToAssetPackDirectoryPath)
{
var sourceAssetsDirectory = new DirectoryInfo(deviceTierAndDirectoryPath.Value);
if (!sourceAssetsDirectory.Exists)
{
// TODO: check this earlier.
return DisplayBuildError("Missing directory for " + assetPackName,
sourceAssetsDirectory.FullName);
}
var targetedAssetsFolderName =
AssetPackFolder + DeviceTierTargetingTools.GetTargetingSuffix(deviceTierAndDirectoryPath.Key);
var outputDirectory = destinationAssetsDirectory.CreateSubdirectory(targetedAssetsFolderName);
CopyFilesRecursively(sourceAssetsDirectory, outputDirectory);
}
}
else
{
throw new InvalidOperationException("Missing asset pack files: " + assetPackName);
}
return null;
}
private void MoveSymbolsZipFile(string aabFilePath)
{
var outputDirectoryPath = Path.GetDirectoryName(aabFilePath);
if (_workingDirectoryPath == outputDirectoryPath)
{
// If the temporary player file and final output file are in the same directory, don't move the symbols.
// (This is likely a Build & Run.)
return;
}
var symbolsFilePath = Path.Combine(_workingDirectoryPath, GetSymbolsFileName(AndroidPlayerFilePrefix));
if (!File.Exists(symbolsFilePath))
{
// The file won't exist for Mono builds or if EditorUserBuildSettings.androidCreateSymbolsZip is false.
return;
}
var outputSymbolsFileName = GetSymbolsFileName(Path.GetFileNameWithoutExtension(aabFilePath));
var outputSymbolsFilePath = Path.Combine(outputDirectoryPath, outputSymbolsFileName);
if (File.Exists(outputSymbolsFilePath))
{
// If the symbols file already exists, we need to delete it first.
File.Delete(outputSymbolsFilePath);
}
File.Move(symbolsFilePath, outputSymbolsFilePath);
}
private string GetSymbolsFileName(string prefix)
{
return string.Format("{0}-{1}-v{2}.symbols.zip", prefix, _versionName, _versionCode);
}
private static void CopyFilesRecursively(DirectoryInfo sourceDirectory, DirectoryInfo destinationDirectory)
{
foreach (var sourceSubdirectory in sourceDirectory.GetDirectories())
{
var destinationSubdirectory = destinationDirectory.CreateSubdirectory(sourceSubdirectory.Name);
CopyFilesRecursively(sourceSubdirectory, destinationSubdirectory);
}
foreach (var file in sourceDirectory.GetFiles())
{
file.CopyTo(Path.Combine(destinationDirectory.FullName, file.Name), false);
}
}
private XDocument CreateAssetPackManifestXDocument(string packName, AssetPackDeliveryMode deliveryMode)
{
if (_assetPackManifestTransformers == null)
{
throw new BuildToolNotInitializedException(this);
}
var doc = AssetPackManifestHelper.CreateAssetPackManifestXDocument(
_packageName,
packName,
deliveryMode);
foreach (var transformer in _assetPackManifestTransformers)
{
var error = transformer.Transform(doc);
if (!string.IsNullOrEmpty(error))
{
DisplayBuildError("AndroidManifest configuration", error);
break;
}
}
return doc;
}
private string CreateBaseModule(DirectoryInfo baseWorkingDirectory, out IList<string> bundleMetadata)
{
DisplayProgress("Creating base module", ProgressCreateBaseModule);
var sourceDirectoryInfo = baseWorkingDirectory.CreateSubdirectory("source");
var unzipErrorMessage = _zipUtils.UnzipFile(AndroidPlayerFilePath, sourceDirectoryInfo.FullName);
if (unzipErrorMessage != null)
{
bundleMetadata = null;
return DisplayBuildError("Unzip", unzipErrorMessage);
}
bundleMetadata = GetExistingBundleMetadata(sourceDirectoryInfo);
var destinationDirectoryInfo = GetDestinationSubdirectory(baseWorkingDirectory);
var baseModuleDirectories = sourceDirectoryInfo.GetDirectories(AndroidAppBundle.BaseModuleName);
if (baseModuleDirectories.Length != 1)
{
return DisplayBuildError("Find base directory", sourceDirectoryInfo.FullName);
}
ArrangeFilesForExistingModule(baseModuleDirectories[0], destinationDirectoryInfo);
return null;
}
private string CreateSplitBaseModule(DirectoryInfo baseDirectory, DirectoryInfo splitBaseDirectory)
{
var aaptErrorMessage =
CreateAssetPackWithManifest(
splitBaseDirectory, AndroidAppBundle.BaseAssetsModuleName, AssetPackDeliveryMode.InstallTime);
if (aaptErrorMessage != null)
{
// Already displayed the error.
return aaptErrorMessage;
}
var baseDestination = GetDestinationSubdirectory(baseDirectory);
var baseAssetsDirectories = baseDestination.GetDirectories(AssetsDirectoryName);
if (baseAssetsDirectories.Length != 1)
{
return DisplayBuildError("Find base assets directory",
string.Format("Expected 1 directory but found {0}", baseAssetsDirectories.Length));
}
var splitBaseDestination = GetDestinationSubdirectory(splitBaseDirectory);
var splitBaseAssetsPath = Path.Combine(splitBaseDestination.FullName, AssetsDirectoryName);
baseAssetsDirectories[0].MoveTo(splitBaseAssetsPath);
// IL2CPP build crash unless the assets/bin/Data/Managed folder exists in the base apk.
// Create an empty file in that folder to ensure that folder is present.
var requiredDirPath = Path.Combine(baseDestination.FullName, RequiredDirectoryName);
var requiredDirInfo = Directory.CreateDirectory(requiredDirPath);
File.Create(Path.Combine(requiredDirInfo.FullName, ".keep_folder")).Close();
return null;
}
// Don't support certain versions of Unity due to the "Failed to load 'libmain.so'" crash.
// See https://github.com/google/play-unity-plugins/issues/80 and
// https://issuetracker.unity3d.com/issues/android-app-installed-using-apk-from-app-bundle-option-in-android-studio-fails-to-run
private static bool CheckUnityVersion(BuildToolLogger buildToolLogger)
{
#if UNITY_2020_2 || UNITY_2020_3_0 || UNITY_2020_3_1 || UNITY_2020_3_2 || UNITY_2021_1_0 || UNITY_2021_1_1
buildToolLogger.DisplayErrorDialog(
"Apps built as AABs with this version of Unity may crash at runtime. Upgrade to 2020.3.3f1, 2021.1.2f1, or later to avoid this issue.");
return false;
#else
return true;
#endif
}
private string CreateAssetPackWithManifest(
DirectoryInfo rootDirectory, string assetPackName, AssetPackDeliveryMode deliveryMode)
{
var androidManifestFilePath = Path.Combine(rootDirectory.FullName, AndroidManifestFileName);
var assetPackManifestXDocument = CreateAssetPackManifestXDocument(assetPackName, deliveryMode);
assetPackManifestXDocument.Save(androidManifestFilePath);
var source = rootDirectory.CreateSubdirectory("manifest");
var aaptErrorMessage = _androidAssetPackagingTool.Link(androidManifestFilePath, source.FullName);
if (aaptErrorMessage != null)
{
return DisplayBuildError("AAPT2 link " + assetPackName, aaptErrorMessage);
}
// aapt2 link creates an empty resource table even though asset packs have no resources.
// Bundletool fails if the asset pack has a resources.pb. Only retain the AndroidManifest.xml file.
var destination = GetDestinationSubdirectory(rootDirectory);
foreach (var sourceFileInfo in source.GetFiles())
{
if (sourceFileInfo.Name == AndroidManifestFileName)
{
var destinationSubdirectory = destination.CreateSubdirectory(ManifestDirectoryName);
sourceFileInfo.MoveTo(Path.Combine(destinationSubdirectory.FullName, sourceFileInfo.Name));
}
}
return null;
}
private static void ArrangeFilesForExistingModule(DirectoryInfo source, DirectoryInfo destination)
{
// Only include the resources.pb file located directly in the source directory.
foreach (var sourceFileInfo in source.GetFiles())
{
if (sourceFileInfo.Name == ResourceTableFileName)
{
sourceFileInfo.MoveTo(Path.Combine(destination.FullName, ResourceTableFileName));
}
}
// Include all directories located in the source directory.
foreach (var sourceDirectoryInfo in source.GetDirectories())
{
sourceDirectoryInfo.MoveTo(Path.Combine(destination.FullName, sourceDirectoryInfo.Name));
}
}
/// <summary>
/// Given the root directory of an existing AAB, return a list of all bundle metadata files in the specific
/// format expected by bundletool.
/// </summary>
private static IList<string> GetExistingBundleMetadata(DirectoryInfo rootDirectoryInfo)
{
var bundleMetadata = new List<string>();
var bundleMetadataDirectories = rootDirectoryInfo.GetDirectories(BundleMetadataDirectoryName);
if (bundleMetadataDirectories.Length != 1)
{
return bundleMetadata;
}
// The metadata files are usually one directory deep, but use Directory.GetFiles() since this isn't a given.
var bundleMetadataPath = bundleMetadataDirectories[0].FullName;
var metadataFiles = Directory.GetFiles(bundleMetadataPath, "*", SearchOption.AllDirectories);
foreach (var physicalFile in metadataFiles)
{
if (physicalFile.Substring(0, bundleMetadataPath.Length) != bundleMetadataPath)
{
throw new InvalidOperationException(
"A bundle metadata file doesn't match the expected parent path: " + physicalFile);
}
var startIndex = bundleMetadataPath.Length + 1;
var relativePath = physicalFile.Substring(startIndex, physicalFile.Length - startIndex);
// Expected format is "<bundle-path>:<physical-file>". "<bundle-path>" uses "/" directory separators.
var bundlePath = relativePath.Replace("\\", "/");
bundleMetadata.Add(string.Format("{0}:{1}", bundlePath, physicalFile));
}
return bundleMetadata;
}
private static DirectoryInfo GetDestinationSubdirectory(DirectoryInfo directoryInfo)
{
return directoryInfo.CreateSubdirectory("destination");
}
private void DisplayProgress(string info, float progress)
{
Debug.LogFormat("{0}...", info);
if (_backgroundThread == null)
{
// Running synchronously in batch mode.
return;
}
_progressBarMessage = info;
_progressBarProgress = progress;
// Wait for the main thread to display the message in at least one callback of the update handler.
// Give up after a short timeout since this is best effort, and we don't want to block indefinitely.
_progressBarWaitHandle.WaitOne(ProgressBarWaitHandleTimeoutMs);
}
private string DisplayBuildError(string errorType, string errorMessage)
{
if (_buildStatus == BuildStatus.Halted)
{
// Ignore any errors after we've halted, e.g. if the thread abort causes an exception to be thrown.
return null;
}
_buildStatus = BuildStatus.Failing;
_buildErrorMessage = string.Format("{0} failed: {1}", errorType, errorMessage);
Debug.LogError(_buildErrorMessage);
return _buildErrorMessage;
}
}
}