// 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 { /// /// Helper to build an Android App Bundle file on Unity. /// public class AppBundleBuilder : IBuildTool { public delegate void PostBuildCallback(string finishedAabFilePath); /// /// Status for coordinating between the background thread and main thread on async builds. /// 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"; /// /// 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. /// 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 _assetPackManifestTransformers; /// /// Constructor. /// 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); } } /// /// Builds an Android Player with the specified options. /// Note: the specified field is ignored and the /// Android Player is written to a temporary file. /// 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); } /// /// Synchronously builds an AAB given the specified options and existing Android Player on disk. /// /// An error message if there was an error, or null if successful. public string CreateBundle(CreateBundleOptions options) { if (_buildStatus != BuildStatus.Running) { throw new Exception("Unexpected call to CreateBundle() with status: " + _buildStatus); } var moduleDirectoryList = new List(); var workingDirectory = new DirectoryInfo(_workingDirectoryPath); var error = CreateAssetModules(moduleDirectoryList, options.AssetPackConfig, workingDirectory); if (error != null) { return error; } IList 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; } /// /// Asynchronously builds an AAB at the specified path. /// /// The AAB output file path. /// Asset packs to include in the AAB. /// /// Callback that fires with the final aab file location, when the bundle creation succeeds. /// 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 /// /// Synchronously builds an Android Player and then produces a final AAB synchronously or asynchronously, /// as specified. /// /// Options indicating how to build the AAB, including asset packs. /// An async task that provides an AndroidBuildReport. public async Task CreateBundleWithTask(AndroidBuildOptions androidBuildOptions) { var taskCompletionSource = new TaskCompletionSource(); 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; } /// /// Builds an Android App Bundle containing only asset packs. /// public async Task CreateAssetOnlyBundle(AssetOnlyBuildOptions assetOnlyBuildOptions) { var assetOnlyOptions = new AssetOnlyOptions { AppVersions = new List(assetOnlyBuildOptions.AppVersions), AssetVersionTag = assetOnlyBuildOptions.AssetVersionTag }; var createBundleOptions = new CreateBundleOptions { AabFilePath = assetOnlyBuildOptions.LocationPathName, AssetPackConfig = SerializationHelper.DeepCopy(assetOnlyBuildOptions.AssetPackConfig), AssetOnlyOptions = assetOnlyOptions }; var completionSource = new TaskCompletionSource(); 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(); 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( TaskCompletionSource taskCompletionSource, Func 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 moduleDirectoryList, CreateBundleOptions options, DirectoryInfo workingDirectory, out IList bundleMetadata) { // Create base module directory. bundleMetadata = new List(); 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 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 moduleDirectoryList, CreateBundleOptions options, IList bundleMetadata = null) { // Create a ZIP file for each module directory. var moduleFiles = new List(); 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(); } 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 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)); } } /// /// Given the root directory of an existing AAB, return a list of all bundle metadata files in the specific /// format expected by bundletool. /// private static IList GetExistingBundleMetadata(DirectoryInfo rootDirectoryInfo) { var bundleMetadata = new List(); 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 ":". "" 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; } } }