chunk 2: remaining non-audio non-NewImport assets
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// 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.IO;
|
||||
using Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Build tool for running <a href="https://developer.android.com/studio/command-line/aapt2">aapt2</a> commands.
|
||||
/// </summary>
|
||||
public class AndroidAssetPackagingTool : IBuildTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum version of Android SDK Build-Tools that supports "aapt2 link --proto-format".
|
||||
/// </summary>
|
||||
private const string BuildToolsMinimumVersion = "28.0.0";
|
||||
|
||||
private const string BuildToolsDisplayName = "Android SDK Build-Tools";
|
||||
|
||||
private readonly AndroidBuildTools _androidBuildTools;
|
||||
private readonly AndroidSdkPlatform _androidSdkPlatform;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public AndroidAssetPackagingTool(AndroidBuildTools androidBuildTools, AndroidSdkPlatform androidSdkPlatform)
|
||||
{
|
||||
_androidBuildTools = androidBuildTools;
|
||||
_androidSdkPlatform = androidSdkPlatform;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (!_androidBuildTools.Initialize(buildToolLogger) || !_androidSdkPlatform.Initialize(buildToolLogger))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var newestBuildToolsVersion = _androidBuildTools.GetNewestBuildToolsVersion();
|
||||
if (newestBuildToolsVersion == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(string.Format("Failed to locate {0}", BuildToolsDisplayName));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AndroidBuildTools.IsBuildToolsVersionAtLeast(newestBuildToolsVersion, BuildToolsMinimumVersion))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var message =
|
||||
string.Format(
|
||||
"This build requires {0} version {1} or later.", BuildToolsDisplayName, BuildToolsMinimumVersion);
|
||||
buildToolLogger.DisplayErrorDialog(message);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the AndroidManifest.xml file path, creates an APK whose
|
||||
/// files are exploded into the specified output directory path.
|
||||
/// </summary>
|
||||
/// <returns>An error message if there was a problem running aapt2, or null if successful.</returns>
|
||||
public virtual string Link(string manifestPath, string outputPath)
|
||||
{
|
||||
return Run(
|
||||
"link -I {0} --manifest {1} --proto-format --output-to-dir -o {2}",
|
||||
CommandLine.QuotePath(GetAndroidJarPath()),
|
||||
CommandLine.QuotePath(manifestPath),
|
||||
CommandLine.QuotePath(outputPath));
|
||||
}
|
||||
|
||||
private string Run(string aaptCommand, params object[] args)
|
||||
{
|
||||
var aaptPath = Path.Combine(_androidBuildTools.GetNewestBuildToolsPath(), "aapt2");
|
||||
var result = CommandLine.Run(aaptPath, string.Format(aaptCommand, args));
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
|
||||
private string GetAndroidJarPath()
|
||||
{
|
||||
var newestPlatformPath = _androidSdkPlatform.GetNewestAndroidSdkPlatformPath();
|
||||
if (newestPlatformPath == null)
|
||||
{
|
||||
throw new Exception("Unable to locate the latest version of the Android SDK Platform.");
|
||||
}
|
||||
|
||||
var androidJarPath = Path.Combine(newestPlatformPath, "android.jar");
|
||||
if (!File.Exists(androidJarPath))
|
||||
{
|
||||
throw new Exception("Unable to locate android.jar in path:" + androidJarPath);
|
||||
}
|
||||
|
||||
return androidJarPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c349f457532c645d8817661cbdcb5729
|
||||
timeCreated: 1556911730
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Google.Android.AppBundle.Editor.Internal.Utils;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods related to Android SDK build-tools.
|
||||
/// </summary>
|
||||
public class AndroidBuildTools : IBuildTool
|
||||
{
|
||||
private const long VersionComponentMultiplier = 10000L;
|
||||
private const long ReleaseCandidateComponentSubtractor = 5000L;
|
||||
|
||||
private static readonly Regex VersionRegex = RegexHelper.CreateCompiled(@"^(\d+)\.(\d+)\.(\d+)(-rc(\d+))?$");
|
||||
|
||||
private readonly AndroidSdk _androidSdk;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor. Must be called from the main thread.
|
||||
/// </summary>
|
||||
public AndroidBuildTools(AndroidSdk androidSdk)
|
||||
{
|
||||
_androidSdk = androidSdk;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
return _androidSdk.Initialize(buildToolLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the path to the newest version of build-tools path as a string, or null if it wasn't found.
|
||||
/// </summary>
|
||||
public virtual string GetNewestBuildToolsPath()
|
||||
{
|
||||
var newestBuildToolsVersion = GetNewestBuildToolsVersion();
|
||||
return newestBuildToolsVersion == null ? null : Path.Combine(GetBuildToolsPath(), newestBuildToolsVersion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the newest build-tools version as a string, or null if one couldn't be found.
|
||||
/// </summary>
|
||||
public virtual string GetNewestBuildToolsVersion()
|
||||
{
|
||||
var buildToolsPath = GetBuildToolsPath();
|
||||
if (!Directory.Exists(buildToolsPath))
|
||||
{
|
||||
Debug.LogErrorFormat("Failed to locate build-tools path: {0}", buildToolsPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(buildToolsPath);
|
||||
var directoryNames = directoryInfo.GetDirectories().Select(dir => dir.Name);
|
||||
var newestBuildTools = GetNewestVersion(directoryNames);
|
||||
if (newestBuildTools == null)
|
||||
{
|
||||
Debug.LogErrorFormat("Failed to locate newest build-tools: {0}", buildToolsPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
return newestBuildTools;
|
||||
}
|
||||
|
||||
private string GetBuildToolsPath()
|
||||
{
|
||||
return Path.Combine(_androidSdk.RootPath, "build-tools");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the specified existing version is the same as or newer than the specified minimum version,
|
||||
/// and false otherwise.
|
||||
/// </summary>
|
||||
public static bool IsBuildToolsVersionAtLeast(string existingVersion, string minimumRequiredVersion)
|
||||
{
|
||||
return ConvertVersionStringToLong(existingVersion) >= ConvertVersionStringToLong(minimumRequiredVersion);
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
public static string GetNewestVersion(IEnumerable<string> versions)
|
||||
{
|
||||
var maxVersionLong = -1L;
|
||||
string maxVersionString = null;
|
||||
foreach (var versionString in versions)
|
||||
{
|
||||
var versionLong = ConvertVersionStringToLong(versionString);
|
||||
if (versionLong > maxVersionLong)
|
||||
{
|
||||
maxVersionLong = versionLong;
|
||||
maxVersionString = versionString;
|
||||
}
|
||||
}
|
||||
|
||||
return maxVersionString;
|
||||
}
|
||||
|
||||
private static long ConvertVersionStringToLong(string versionString)
|
||||
{
|
||||
var match = VersionRegex.Match(versionString);
|
||||
if (!match.Success)
|
||||
{
|
||||
return -1L;
|
||||
}
|
||||
|
||||
var versionLong = 0L;
|
||||
for (var i = 1; i <= 3; i++)
|
||||
{
|
||||
var versionComponent = long.Parse(match.Groups[i].Value);
|
||||
if (versionComponent >= VersionComponentMultiplier)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format("Component {0} from {1} exceeds the limit.", versionComponent, versionString),
|
||||
"versionString");
|
||||
}
|
||||
|
||||
versionLong += versionComponent;
|
||||
// Multiply by a somewhat arbitrary value since major version outweighs minor version.
|
||||
// This particular arbitrary value supports up to 4 digits per version component.
|
||||
versionLong *= VersionComponentMultiplier;
|
||||
}
|
||||
|
||||
var releaseCandidateVersionGroup = match.Groups[5];
|
||||
if (releaseCandidateVersionGroup.Success)
|
||||
{
|
||||
var releaseCandidateVersion = long.Parse(releaseCandidateVersionGroup.Value);
|
||||
if (releaseCandidateVersion >= ReleaseCandidateComponentSubtractor)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format("rc{0} from {1} exceeds the limit.", releaseCandidateVersion, versionString),
|
||||
"versionString");
|
||||
}
|
||||
|
||||
// Add the release candidate version, e.g. rc2 is newer than rc1.
|
||||
versionLong += releaseCandidateVersion;
|
||||
// But also subtract a little, since any "rc" is earlier than the equivalent release,
|
||||
// e.g. "28.0.0-rc2" is older than "28.0.0".
|
||||
versionLong -= ReleaseCandidateComponentSubtractor;
|
||||
}
|
||||
|
||||
return versionLong;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46ae92b3dd8eb4e38b66ff3b58205318
|
||||
timeCreated: 1556911730
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,257 @@
|
||||
// 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.IO;
|
||||
using Google.Android.AppBundle.Editor.Internal.AssetPacks;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if UNITY_2018_1_OR_NEWER
|
||||
using UnityEditor.Build.Reporting;
|
||||
|
||||
#endif
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the results of an <see cref="AndroidBuilder"/> build. This provides similar information as
|
||||
/// Task<AndroidBuildReport> without requiring the .NET 4.0 async task API.
|
||||
/// </summary>
|
||||
public class AndroidBuildResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if Android Player build Succeeded, otherwise false.
|
||||
/// </summary>
|
||||
public bool Succeeded
|
||||
{
|
||||
get { return !Cancelled && ErrorMessage == null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the Android Player build was cancelled before it finished.
|
||||
/// </summary>
|
||||
public bool Cancelled { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// If non-null, the build failed and this message may indicate the cause.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; internal set; }
|
||||
|
||||
#if UNITY_2018_1_OR_NEWER
|
||||
/// <summary>
|
||||
/// Contains the BuildReport generated during the creation of the Android Player.
|
||||
/// </summary>
|
||||
public BuildReport Report { get; internal set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for building an Android app for distribution on Google Play.
|
||||
/// </summary>
|
||||
public class AndroidBuilder : IBuildTool
|
||||
{
|
||||
// This string is returned by pre-2018.1's BuildPipeline.BuildPlayer() when a build is cancelled.
|
||||
private const string BuildCancelledMessage = "Building Player was cancelled";
|
||||
|
||||
private readonly AndroidSdkPlatform _androidSdkPlatform;
|
||||
|
||||
private BuildToolLogger _buildToolLogger;
|
||||
|
||||
public AndroidBuilder(AndroidSdkPlatform androidSdkPlatform)
|
||||
{
|
||||
_androidSdkPlatform = androidSdkPlatform;
|
||||
}
|
||||
|
||||
// TODO(b/189958664): Needed for 1.x API compatibility. Should be removed with 2.x.
|
||||
public AndroidBuilder(AndroidSdkPlatform androidSdkPlatform, ApkSigner apkSigner) : this(androidSdkPlatform)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
_buildToolLogger = buildToolLogger;
|
||||
|
||||
if (EditorUserBuildSettings.androidBuildSystem != AndroidBuildSystem.Gradle)
|
||||
{
|
||||
// We require Gradle builds for APK Signature Scheme V2, which is required for Play Instant on Unity 2017+.
|
||||
const string message = "This build requires the Gradle Build System.\n\n" +
|
||||
"Click \"OK\" to change the Android Build System to Gradle.";
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(message))
|
||||
{
|
||||
EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(b/154937088): remove this check if we add support for exporting a Gradle project.
|
||||
if (EditorUserBuildSettings.exportAsGoogleAndroidProject)
|
||||
{
|
||||
const string message = "This build doesn't support exporting to Android Studio.\n\n" +
|
||||
"Click \"OK\" to disable exporting a Gradle project.";
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(message))
|
||||
{
|
||||
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BuiltInPadHelper.EditorSupportsPad() && BuiltInPadHelper.ProjectHasAndroidPacks())
|
||||
{
|
||||
var message =
|
||||
"This build method doesn't support .androidpack folders. Assets within those folders will not be included" +
|
||||
" in the build.";
|
||||
buildToolLogger.DisplayOptOutDialog(message, "androidPackError");
|
||||
}
|
||||
|
||||
if (PlayerSettings.Android.splitApplicationBinary)
|
||||
{
|
||||
string messagePrefix;
|
||||
if (BuiltInPadHelper.EditorSupportsPad())
|
||||
{
|
||||
messagePrefix = "This build method doesn't support Unity's \"Split Application Binary\" option.";
|
||||
}
|
||||
else
|
||||
{
|
||||
messagePrefix = "This build method doesn't support APK Expansion (OBB) files.";
|
||||
}
|
||||
|
||||
var message = string.Format(
|
||||
"{0}\n\nLarge games can instead use the " +
|
||||
"\"{1}\" option available through the \"Google > Android App Bundle > Asset Delivery " +
|
||||
"Settings\" menu or the AssetPackConfig API's SplitBaseModuleAssets field.\n\n" +
|
||||
"Click \"OK\" to disable the \"Split Application Binary\" setting.",
|
||||
messagePrefix,
|
||||
AssetDeliveryWindow.SeparateAssetsLabel);
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(message))
|
||||
{
|
||||
PlayerSettings.Android.splitApplicationBinary = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return _androidSdkPlatform.Initialize(buildToolLogger);
|
||||
}
|
||||
|
||||
// TODO(b/189958664): Needed for 1.x API compatibility. Should be removed with 2.x.
|
||||
public virtual bool BuildAndSign(BuildPlayerOptions buildPlayerOptions)
|
||||
{
|
||||
return Build(buildPlayerOptions).Succeeded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an APK or AAB based on the specified options.
|
||||
/// Displays warning/error dialogs if there are issues during the build.
|
||||
/// </summary>
|
||||
public virtual AndroidBuildResult Build(BuildPlayerOptions buildPlayerOptions)
|
||||
{
|
||||
if (_buildToolLogger == null)
|
||||
{
|
||||
throw new BuildToolNotInitializedException(this);
|
||||
}
|
||||
|
||||
if (buildPlayerOptions.target != BuildTarget.Android)
|
||||
{
|
||||
throw new ArgumentException("The build target must be Android.", "buildPlayerOptions");
|
||||
}
|
||||
|
||||
if (buildPlayerOptions.targetGroup != BuildTargetGroup.Android)
|
||||
{
|
||||
throw new ArgumentException("The build target group must be Android.", "buildPlayerOptions");
|
||||
}
|
||||
|
||||
// Note: the type of the variable below differs by version. On 2018+ it's BuildReport. On pre-2018 it's
|
||||
// string: if the string is null, the build was successful, otherwise it's a build error message.
|
||||
var buildReportOrErrorMessage = BuildPipeline.BuildPlayer(buildPlayerOptions);
|
||||
var androidBuildResult = GetAndroidBuildResult(buildReportOrErrorMessage);
|
||||
if (androidBuildResult.Cancelled)
|
||||
{
|
||||
// Don't display an error message dialog if the user asked to cancel, just log to the Console.
|
||||
Debug.Log(BuildCancelledMessage);
|
||||
return androidBuildResult;
|
||||
}
|
||||
|
||||
if (androidBuildResult.Succeeded && !File.Exists(buildPlayerOptions.locationPathName))
|
||||
{
|
||||
// Sometimes the build "succeeds" but the AAB/APK file is missing.
|
||||
androidBuildResult.ErrorMessage =
|
||||
string.Format(
|
||||
"The Android Player file \"{0}\" is missing, possibly because of a late cancellation.",
|
||||
buildPlayerOptions.locationPathName);
|
||||
#if UNITY_2018_1_OR_NEWER
|
||||
androidBuildResult.ErrorMessage += " TotalErrors=" + buildReportOrErrorMessage.summary.totalErrors;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (androidBuildResult.Succeeded)
|
||||
{
|
||||
Debug.Log("Android Player build succeeded");
|
||||
}
|
||||
else
|
||||
{
|
||||
_buildToolLogger.DisplayErrorDialog(androidBuildResult.ErrorMessage);
|
||||
}
|
||||
|
||||
return androidBuildResult;
|
||||
}
|
||||
|
||||
#if UNITY_2018_1_OR_NEWER
|
||||
private static AndroidBuildResult GetAndroidBuildResult(BuildReport buildReport)
|
||||
{
|
||||
var androidBuildResult = new AndroidBuildResult();
|
||||
androidBuildResult.Report = buildReport;
|
||||
switch (buildReport.summary.result)
|
||||
{
|
||||
case BuildResult.Succeeded:
|
||||
// Do nothing.
|
||||
break;
|
||||
case BuildResult.Cancelled:
|
||||
androidBuildResult.Cancelled = true;
|
||||
break;
|
||||
case BuildResult.Failed:
|
||||
androidBuildResult.ErrorMessage =
|
||||
string.Format("Build failed with {0} error(s)", buildReport.summary.totalErrors);
|
||||
break;
|
||||
case BuildResult.Unknown:
|
||||
androidBuildResult.ErrorMessage = "Build failed with unknown result";
|
||||
break;
|
||||
default:
|
||||
androidBuildResult.ErrorMessage =
|
||||
"Build failed with unexpected result: " + buildReport.summary.result;
|
||||
break;
|
||||
}
|
||||
|
||||
return androidBuildResult;
|
||||
}
|
||||
#else
|
||||
private static AndroidBuildResult GetAndroidBuildResult(string errorMessage)
|
||||
{
|
||||
var androidBuildResult = new AndroidBuildResult();
|
||||
if (errorMessage == BuildCancelledMessage)
|
||||
{
|
||||
androidBuildResult.Cancelled = true;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
// Assume that a null or empty error message string indicates success.
|
||||
androidBuildResult.ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
return androidBuildResult;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24e34b4393a4d4a5d8de83ca5e08621c
|
||||
timeCreated: 1556919494
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 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.IO;
|
||||
using Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for <a href="https://developer.android.com/studio/command-line/adb">adb</a>.
|
||||
/// </summary>
|
||||
public class AndroidDebugBridge : IBuildTool
|
||||
{
|
||||
private const string AdbName = "adb";
|
||||
private const string AdbDirectory = "platform-tools";
|
||||
private readonly AndroidSdk _androidSdk;
|
||||
private string _adbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public AndroidDebugBridge(AndroidSdk androidSdk)
|
||||
{
|
||||
_androidSdk = androidSdk;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (!_androidSdk.Initialize(buildToolLogger))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_adbPath = GetAdbPath();
|
||||
return _adbPath != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Launches the default activity in the specified package.
|
||||
/// Assumes that there is exactly one adb connected device.
|
||||
/// </summary>
|
||||
/// <param name="packageName">The name of the package to be launched e.g. "com.package.name".</param>
|
||||
/// <returns>An error message if the command failed or null if successful.</returns>
|
||||
public string LaunchApp(string packageName)
|
||||
{
|
||||
// Run the Monkey tool restricted to packageName and with 1 event, thereby launching the app.
|
||||
// See https://developer.android.com/studio/test/monkey.html
|
||||
return Run("shell monkey -p {0} 1", packageName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the adb executable associated with the androidSdk object passed into Initialize().
|
||||
/// </summary>
|
||||
public string GetAdbPath()
|
||||
{
|
||||
var adbPath = Path.Combine(_androidSdk.RootPath,
|
||||
Path.Combine(AdbDirectory, AdbName + CommandLine.GetExecutableExtension()));
|
||||
if (File.Exists(adbPath))
|
||||
{
|
||||
return adbPath;
|
||||
}
|
||||
|
||||
adbPath = CommandLine.FindExecutable(AdbName);
|
||||
return File.Exists(adbPath) ? adbPath : null;
|
||||
}
|
||||
|
||||
private string Run(string adbCommand, params object[] args)
|
||||
{
|
||||
var adbCommandWithArgs = string.Format(adbCommand, args);
|
||||
var result = CommandLine.Run(_adbPath, adbCommandWithArgs);
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a5aaac6c5d78491aa3590cbff08a890
|
||||
timeCreated: 1556913938
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2019 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.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Build tool for finding the Android SDK root path.
|
||||
/// </summary>
|
||||
public class AndroidSdk : IBuildTool
|
||||
{
|
||||
/// <summary>
|
||||
/// The ANDROID_HOME environment variable key.
|
||||
/// </summary>
|
||||
public const string AndroidHomeEnvironmentVariableKey = "ANDROID_HOME";
|
||||
|
||||
private const string AndroidNdkHomeEnvironmentVariableKey = "ANDROID_NDK_HOME";
|
||||
|
||||
/// <summary>
|
||||
/// The environment variable key we use when overriding existing Unity editor preferences.
|
||||
/// We use UNITY_JAVA_HOME instead of JAVA_HOME because Unity will override JAVA_HOME to match it's editor preferences.
|
||||
/// </summary>
|
||||
private const string JavaHomeEnvironmentVariableKey = "UNITY_JAVA_HOME";
|
||||
private const string AndroidSdkRootEditorPrefsKey = "AndroidSdkRoot";
|
||||
private const string AndroidNdkRootEditorPrefsKey = "AndroidNdkRoot";
|
||||
private const string JdkPathEditorPrefsKey = "JdkPath";
|
||||
private const string JdkUseEmbeddedEditorPrefsKey = "JdkUseEmbedded";
|
||||
|
||||
private string _androidSdkRoot;
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
#if !UNITY_2019_3_OR_NEWER
|
||||
return SetSdkRoot(buildToolLogger, EditorPrefs.GetString(AndroidSdkRootEditorPrefsKey));
|
||||
#elif UNITY_ANDROID
|
||||
// Guard with #if UNITY_ANDROID since UnityEditor.Android.Extensions.dll might be unavailable,
|
||||
// e.g. on a build machine without the Android platform installed.
|
||||
return SetSdkRoot(buildToolLogger, UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath);
|
||||
#else
|
||||
// This would be an unexpected and hopefully transient error.
|
||||
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog("ActiveBuildTarget disagrees with #if UNITY_ANDROID.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle the case where the UnityEditor.Android.Extensions.dll file may not be available by requiring that
|
||||
// Android be the selected platform on 2019.3+.
|
||||
const string message = "The Build Settings > Platform must be set to Android." +
|
||||
"\n\nClick \"OK\" to switch the active platform to Android.";
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(message))
|
||||
{
|
||||
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);
|
||||
}
|
||||
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the AndroidSdkRoot from Unity preferences or from the ANDROID_HOME environment variable.
|
||||
/// </summary>
|
||||
public virtual string RootPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_androidSdkRoot == null)
|
||||
{
|
||||
throw new BuildToolNotInitializedException(this);
|
||||
}
|
||||
|
||||
return _androidSdkRoot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override the AndroidSdkRoot/AndroidNdkRoot/JdkPath EditorPrefs with corresponding environment variables.
|
||||
/// This is especially helpful for automated builds where the preferences may not be set.
|
||||
/// </summary>
|
||||
public static void OverrideEditorPreferences()
|
||||
{
|
||||
#if !UNITY_ANDROID
|
||||
return;
|
||||
#elif UNITY_2019_3_OR_NEWER
|
||||
OverrideEditorPreference(AndroidHomeEnvironmentVariableKey,
|
||||
() => UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath,
|
||||
v => { UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath = v; });
|
||||
OverrideEditorPreference(AndroidNdkHomeEnvironmentVariableKey,
|
||||
() => UnityEditor.Android.AndroidExternalToolsSettings.ndkRootPath,
|
||||
v => { UnityEditor.Android.AndroidExternalToolsSettings.ndkRootPath = v; });
|
||||
OverrideEditorPreference(JavaHomeEnvironmentVariableKey,
|
||||
() => UnityEditor.Android.AndroidExternalToolsSettings.jdkRootPath,
|
||||
v => { UnityEditor.Android.AndroidExternalToolsSettings.jdkRootPath = v; });
|
||||
#else
|
||||
OverrideEditorPreference(AndroidHomeEnvironmentVariableKey,
|
||||
() => EditorPrefs.GetString(AndroidSdkRootEditorPrefsKey),
|
||||
v => { EditorPrefs.SetString(AndroidSdkRootEditorPrefsKey, v); });
|
||||
OverrideEditorPreference(AndroidNdkHomeEnvironmentVariableKey,
|
||||
() => EditorPrefs.GetString(AndroidNdkRootEditorPrefsKey),
|
||||
v => { EditorPrefs.SetString(AndroidNdkRootEditorPrefsKey, v); });
|
||||
|
||||
OverrideEditorPreference(JavaHomeEnvironmentVariableKey,
|
||||
() => EditorPrefs.GetString(JdkPathEditorPrefsKey),
|
||||
v => { EditorPrefs.SetString(JdkPathEditorPrefsKey, v); });
|
||||
|
||||
// Older versions of Unity have an additional boolean preference that must be disabled.
|
||||
// Otherwise, Unity will use the JDK installed with the editor instead of the path we set in EditorPrefs.
|
||||
EditorPrefs.SetInt(JdkUseEmbeddedEditorPrefsKey, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// TODO(b/189958664): Move this to a helper class and move the JDK portion of OverrideEditorPreferences into JavaUtils.
|
||||
private static void OverrideEditorPreference(
|
||||
string environmentVariableKey, Func<string> getPreference, Action<string> setPreference)
|
||||
{
|
||||
var environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableKey);
|
||||
var existingPreferenceValue = getPreference();
|
||||
Debug.LogFormat("Existing preference for {0} is \"{1}\"", environmentVariableKey, existingPreferenceValue);
|
||||
if (environmentVariableValue == existingPreferenceValue)
|
||||
{
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(environmentVariableValue))
|
||||
{
|
||||
Debug.LogWarningFormat("Skipping empty environment variable: {0}", environmentVariableKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogFormat("Setting preference for {0} to {1}", environmentVariableKey, environmentVariableValue);
|
||||
setPreference(environmentVariableValue);
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetSdkRoot(BuildToolLogger buildToolLogger, string sdkPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sdkPath))
|
||||
{
|
||||
sdkPath = Environment.GetEnvironmentVariable(AndroidHomeEnvironmentVariableKey);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(sdkPath))
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate the Android SDK. Check Preferences -> External Tools to set the path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_androidSdkRoot = sdkPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 408798f1df959420d8fb95911a935f2d
|
||||
timeCreated: 1556899571
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019 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.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Google.Android.AppBundle.Editor.Internal.Utils;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Build tool for discovering Android SDK Platform versions.
|
||||
/// </summary>
|
||||
public class AndroidSdkPlatform : IBuildTool
|
||||
{
|
||||
// The minimum Android target SDK version supported by Google Play is described here:
|
||||
// https://support.google.com/googleplay/android-developer/answer/113469#targetsdk
|
||||
private const int MinimumVersion = 30;
|
||||
private const int LatestVersion = 31;
|
||||
|
||||
private static readonly Regex PlatformVersionRegex = RegexHelper.CreateCompiled(@"^android-(\d+)$");
|
||||
|
||||
private readonly AndroidSdk _androidSdk;
|
||||
|
||||
public AndroidSdkPlatform(AndroidSdk androidSdk)
|
||||
{
|
||||
_androidSdk = androidSdk;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (!_androidSdk.Initialize(buildToolLogger))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If targetSdkVersion is higher than the MinimumVersion, we skip verifying the newest version,
|
||||
// and instead trust that Unity will install it at build time if it isn't already available.
|
||||
var targetSdkVersion = PlayerSettings.Android.targetSdkVersion;
|
||||
if ((int)targetSdkVersion >= MinimumVersion)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string ignoredPath;
|
||||
var newestVersion = GetNewestVersionAndPath(out ignoredPath);
|
||||
if (newestVersion == null)
|
||||
{
|
||||
// Being unable to find any existing SDK may indicate a config issue, so don't try to install a new SDK.
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate any Android SDK Platform version. Check that the Android SDK specified "
|
||||
+ "through Preferences -> External Tools has at least one Android SDK Platform installed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newestVersion < MinimumVersion)
|
||||
{
|
||||
var installedVersionMessage = string.Format(
|
||||
"The highest installed Android API Level is {0}, however version {1} is the minimum "
|
||||
+ "required to build for Google Play.\n\nClick \"OK\" to install Android API Level {2}.",
|
||||
newestVersion, MinimumVersion, LatestVersion);
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(installedVersionMessage))
|
||||
{
|
||||
// Note: this install can be slow, but it's not clear that it's any slower through Unity.
|
||||
AndroidSdkPackageInstaller.InstallPackage(
|
||||
string.Format("platforms;android-{0}", LatestVersion),
|
||||
string.Format("Android SDK Platform {0}", LatestVersion),
|
||||
_androidSdk.RootPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetSdkVersion == AndroidSdkVersions.AndroidApiLevelAuto)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var selectedVersionMessage = string.Format(
|
||||
"The currently selected Android Target API Level is {0}, however version {1} is the minimum "
|
||||
+ "required to build for Google Play.\n\nClick \"OK\" to change the Target API Level to "
|
||||
+ "\"Automatic (highest installed)\", which is currently {2}.",
|
||||
(int)targetSdkVersion, MinimumVersion, newestVersion);
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(selectedVersionMessage))
|
||||
{
|
||||
PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevelAuto;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the path to the newest installed version of the Android SDK Platform, or null if it isn't found.
|
||||
/// </summary>
|
||||
public virtual string GetNewestAndroidSdkPlatformPath()
|
||||
{
|
||||
string newestPlatformPath;
|
||||
GetNewestVersionAndPath(out newestPlatformPath);
|
||||
return newestPlatformPath;
|
||||
}
|
||||
|
||||
private int? GetNewestVersionAndPath(out string newestPlatformPath)
|
||||
{
|
||||
newestPlatformPath = null;
|
||||
var platformsPath = Path.Combine(_androidSdk.RootPath, "platforms");
|
||||
var platformsDirectoryInfo = new DirectoryInfo(platformsPath);
|
||||
if (!platformsDirectoryInfo.Exists)
|
||||
{
|
||||
Debug.LogErrorFormat("Unable to locate Android SDK platforms directory: {0}", platformsPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
int? newestPlatformVersion = null;
|
||||
DirectoryInfo newestPlatformDirectory = null;
|
||||
foreach (var platformDirectory in platformsDirectoryInfo.GetDirectories())
|
||||
{
|
||||
var match = PlatformVersionRegex.Match(platformDirectory.Name);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var platformVersionString = match.Groups[1].Value;
|
||||
var platformVersion = int.Parse(platformVersionString);
|
||||
newestPlatformVersion = Math.Max(platformVersion, newestPlatformVersion ?? -1);
|
||||
if (platformVersion == newestPlatformVersion)
|
||||
{
|
||||
newestPlatformDirectory = platformDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
if (newestPlatformDirectory == null)
|
||||
{
|
||||
Debug.LogErrorFormat("Unable to locate newest Android SDK platform in directory: {0}", platformsPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
newestPlatformPath = newestPlatformDirectory.FullName;
|
||||
return newestPlatformVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36e42f83ed7a745ea9e4f4166d786141
|
||||
timeCreated: 1556910452
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provided methods on 2017.4 and earlier that call the Android SDK build tool "apksigner" to verify whether an APK
|
||||
/// complies with <a href="https://source.android.com/security/apksigning/v2">APK Signature Scheme V2</a>.
|
||||
/// </summary>
|
||||
// TODO(b/189958664): Needed for 1.x API compatibility. Should be removed with 2.x.
|
||||
public class ApkSigner : IBuildTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public ApkSigner(AndroidBuildTools androidBuildTools, JavaUtils javaUtils)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 960e36d9911aa4011958de1e7407757d
|
||||
timeCreated: 1556916021
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,992 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4ee87517b6d8405f9af0cd4e6d2029e
|
||||
timeCreated: 1556919555
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2019 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.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper to build an Android App Bundle file on Unity.
|
||||
/// </summary>
|
||||
public class AppBundleRunner : IBuildTool
|
||||
{
|
||||
private readonly AndroidDebugBridge _adb;
|
||||
private readonly BundletoolHelper _bundletool;
|
||||
private string _packageName;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public AppBundleRunner(AndroidDebugBridge adb, BundletoolHelper bundletool)
|
||||
{
|
||||
_adb = adb;
|
||||
_bundletool = bundletool;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
_packageName = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android);
|
||||
return _adb.Initialize(buildToolLogger) && _bundletool.Initialize(buildToolLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the specified app bundle into an apk set file,
|
||||
/// installs the proper apks,
|
||||
/// then runs the app on device.
|
||||
/// Note: This is designed to run in the main thread.
|
||||
/// TODO(b/139089705): Explore running this in a background thread.
|
||||
/// </summary>
|
||||
public void RunBundle(string aabFilePath, BundletoolBuildMode buildMode)
|
||||
{
|
||||
string apkSetFilePath;
|
||||
var errorMessage = ConvertAabToApkSet(aabFilePath, buildMode, out apkSetFilePath);
|
||||
if (errorMessage != null)
|
||||
{
|
||||
DisplayRunError("Creating apk set", errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
errorMessage = _bundletool.InstallApkSet(apkSetFilePath, _adb.GetAdbPath());
|
||||
if (errorMessage != null)
|
||||
{
|
||||
DisplayRunError("Installing app bundle", errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("Installing app bundle");
|
||||
|
||||
// TODO(b/138958246): Check the number of devices before launching to display a nicer error message.
|
||||
errorMessage = _adb.LaunchApp(_packageName);
|
||||
if (errorMessage != null)
|
||||
{
|
||||
DisplayRunError("Launching app bundle", errorMessage);
|
||||
}
|
||||
|
||||
Debug.Log("Launching app bundle");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the specified app bundle into an apk set file.
|
||||
/// </summary>
|
||||
/// <returns>An error message if the operation failed and null otherwise.</returns>
|
||||
public string ConvertAabToApkSet(string aabFilePath, BundletoolBuildMode buildMode, out string apkSetFilePath)
|
||||
{
|
||||
var aabFileDirectory = Path.GetDirectoryName(aabFilePath);
|
||||
if (aabFileDirectory == null)
|
||||
{
|
||||
apkSetFilePath = null;
|
||||
return "App Bundle does not exist at path " + aabFilePath;
|
||||
}
|
||||
|
||||
var apkSetFileName = Path.GetFileNameWithoutExtension(aabFilePath);
|
||||
apkSetFilePath = Path.Combine(aabFileDirectory, apkSetFileName + ".apks");
|
||||
File.Delete(apkSetFilePath);
|
||||
|
||||
// TODO(b/149439143): Set this value to be true regardless of BuildMode once local testing works on instant.
|
||||
var enableLocalTesting = buildMode == BundletoolBuildMode.Persistent;
|
||||
|
||||
return _bundletool.BuildApkSet(aabFilePath, apkSetFilePath, buildMode, enableLocalTesting);
|
||||
}
|
||||
|
||||
// TODO(b/138958246): Display a dialog prompt and/or progress bar.
|
||||
private void DisplayRunError(string errorType, string errorMessage)
|
||||
{
|
||||
var fullErrorMessage = string.Format("{0} failed: {1}", errorType, errorMessage);
|
||||
Debug.LogError(fullErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8103f73350aca4529af9e65605055a8b
|
||||
timeCreated: 1540331104
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019 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 Google.Android.AppBundle.Editor.Internal.Utils;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for logging errors and/or displaying dialogs if there is a build issue.
|
||||
/// </summary>
|
||||
public class BuildToolLogger
|
||||
{
|
||||
public const string BuildErrorTitle = "Build Error";
|
||||
public const string BuildWarningTitle = "Build Warning";
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the running instance of Unity is headless (e.g. a command line build),
|
||||
/// and false if it's a normal instance of Unity.
|
||||
/// </summary>
|
||||
public virtual bool IsHeadlessMode()
|
||||
{
|
||||
return WindowUtils.IsHeadlessMode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the specified message indicating that a build error occurred. Displays an "OK" button that can
|
||||
/// be used to indicate that the user wants to perform a followup action, e.g. fixing a build setting.
|
||||
/// </summary>
|
||||
/// <returns>True if the user clicks "OK", otherwise false.</returns>
|
||||
public virtual bool DisplayActionableErrorDialog(string message)
|
||||
{
|
||||
Debug.LogErrorFormat("Actionable build error: {0}", message);
|
||||
|
||||
if (IsHeadlessMode())
|
||||
{
|
||||
// During a headless build it isn't possible to prompt to fix the issue, so always return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
return EditorUtility.DisplayDialog(
|
||||
BuildErrorTitle, message, WindowUtils.OkButtonText, WindowUtils.CancelButtonText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays dialog with an additional option to "Opt out for this session". If the user selects that setting,
|
||||
/// they won't be shown the dialog again until they restart Unity. Instead, the specified message will be logged
|
||||
/// as a warning.
|
||||
///
|
||||
/// On Unity versions 2019.2 and below, this method displays a normal dialog without the opt out option.
|
||||
/// </summary>
|
||||
/// <param name="message">The message included in the dialog.</param>
|
||||
/// <param name="optOutPreferenceKey">The unique key used to determine if a user has opted out of this dialog.</param>
|
||||
public virtual void DisplayOptOutDialog(string message, string optOutPreferenceKey)
|
||||
{
|
||||
Debug.LogWarningFormat("Build warning: {0}", message);
|
||||
if (IsHeadlessMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
var didOptOut =
|
||||
EditorUtility.GetDialogOptOutDecision(DialogOptOutDecisionType.ForThisSession, optOutPreferenceKey);
|
||||
if (didOptOut)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
EditorUtility.DisplayDialog(BuildWarningTitle, message, WindowUtils.OkButtonText,
|
||||
DialogOptOutDecisionType.ForThisSession, optOutPreferenceKey);
|
||||
#else
|
||||
EditorUtility.DisplayDialog(BuildWarningTitle, message, WindowUtils.OkButtonText);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the specified message indicating that a build error occurred.
|
||||
/// </summary>
|
||||
public virtual void DisplayErrorDialog(string message)
|
||||
{
|
||||
Debug.LogErrorFormat("Build error: {0}", message);
|
||||
|
||||
if (!IsHeadlessMode())
|
||||
{
|
||||
EditorUtility.DisplayDialog(BuildErrorTitle, message, WindowUtils.OkButtonText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fb883fe4e14d4d90a8d664e93cb0b81
|
||||
timeCreated: 1556899571
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2019 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;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception thrown if an <see cref="IBuildTool"/> method is called before <see cref="IBuildTool.Initialize"/>.
|
||||
/// </summary>
|
||||
public class BuildToolNotInitializedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public BuildToolNotInitializedException(IBuildTool buildTool) :
|
||||
base(string.Format("Call {0}.Initialize() before calling this method/property.", buildTool.GetType().Name))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09bfd5773b8db4b3ebc596a1c11cae96
|
||||
timeCreated: 1556899570
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2019 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;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a C# mirror of
|
||||
/// <a href="https://github.com/google/bundletool/blob/master/src/main/proto/config.proto">BundletoolConfig</a>
|
||||
/// for use with JsonUtility.
|
||||
/// Inner class fields are camelCase to match json style.
|
||||
/// </summary>
|
||||
public static class BundletoolConfig
|
||||
{
|
||||
// Options for installTimeAssetModuleDefaultCompression
|
||||
public const string Unspecified = "UNSPECIFIED";
|
||||
public const string Compressed = "COMPRESSED";
|
||||
|
||||
public const string Abi = "ABI";
|
||||
public const string Language = "LANGUAGE";
|
||||
public const string ScreenDensity = "SCREEN_DENSITY";
|
||||
public const string TextureCompressionFormat = "TEXTURE_COMPRESSION_FORMAT";
|
||||
public const string DeviceTier = "DEVICE_TIER";
|
||||
|
||||
|
||||
// Options for BundleType
|
||||
public const string Regular = "REGULAR";
|
||||
public const string AssetOnly = "ASSET_ONLY";
|
||||
|
||||
[Serializable]
|
||||
public class Config
|
||||
{
|
||||
public Optimizations optimizations = new Optimizations();
|
||||
public Compression compression = new Compression();
|
||||
public string type = Regular;
|
||||
public AssetModulesConfig asset_modules_config = new AssetModulesConfig();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Optimizations
|
||||
{
|
||||
public SplitsConfig splitsConfig = new SplitsConfig();
|
||||
public UncompressNativeLibraries uncompressNativeLibraries = new UncompressNativeLibraries();
|
||||
public UncompressDexFiles uncompressDexFiles = new UncompressDexFiles();
|
||||
public StandaloneConfig standaloneConfig = new StandaloneConfig();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SplitsConfig
|
||||
{
|
||||
public List<SplitDimension> splitDimension = new List<SplitDimension>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class StandaloneConfig
|
||||
{
|
||||
public List<SplitDimension> splitDimension = new List<SplitDimension>();
|
||||
public bool strip64BitLibraries;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SplitDimension
|
||||
{
|
||||
public string value;
|
||||
public bool negate;
|
||||
public SuffixStripping suffixStripping = new SuffixStripping();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Compression
|
||||
{
|
||||
public List<string> uncompressedGlob = new List<string>();
|
||||
public string installTimeAssetModuleDefaultCompression = Unspecified;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UncompressNativeLibraries
|
||||
{
|
||||
public bool enabled;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UncompressDexFiles
|
||||
{
|
||||
public bool enabled;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SuffixStripping
|
||||
{
|
||||
public bool enabled;
|
||||
public string defaultSuffix;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class AssetModulesConfig
|
||||
{
|
||||
public List<long> app_version = new List<long>();
|
||||
public string asset_version_tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f770d51f7cfa14c9da2a31e08a167e73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for bundletool.
|
||||
/// </summary>
|
||||
public static class BundletoolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the specified <see cref="BundletoolBuildMode"/> as an acceptable value for the "--mode" flag
|
||||
/// of "bundletool build-apks".
|
||||
/// </summary>
|
||||
public static string GetModeFlag(this BundletoolBuildMode buildMode)
|
||||
{
|
||||
if (buildMode == BundletoolBuildMode.SystemCompressed)
|
||||
{
|
||||
return "system_compressed";
|
||||
}
|
||||
|
||||
return buildMode.ToString().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0996c5b6f76c44bab82db6c1f36a84c1
|
||||
timeCreated: 1575644561
|
||||
@@ -0,0 +1,386 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Google.Android.AppBundle.Editor.Internal.AssetPacks;
|
||||
using Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for <a href="https://developer.android.com/studio/command-line/bundletool">bundletool</a>.
|
||||
/// </summary>
|
||||
public class BundletoolHelper : IBuildTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains parameters needed to generate a config file for the build-bundle command.
|
||||
/// </summary>
|
||||
public class BuildBundleConfigParams
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, enables targeting of module contents by texture formats.
|
||||
/// </summary>
|
||||
public bool enableTcfTargeting;
|
||||
|
||||
/// <summary>
|
||||
/// When targeting by texture format, specifies the default format that will be used to generate
|
||||
/// standalone APKs for Android pre-Lollipop devices that don't support split APKs.
|
||||
/// </summary>
|
||||
public string defaultTcfSuffix;
|
||||
|
||||
/// <summary>
|
||||
/// If true, enables targeting of module contents by device tiers.
|
||||
/// </summary>
|
||||
public bool enableDeviceTierTargeting;
|
||||
|
||||
/// <summary>
|
||||
/// When targeting by device tier, specifies the default device tier that will be used to generate
|
||||
/// standalone APKs for Android pre-Lollipop devices that don't support split APKs.
|
||||
/// If not specified, it defaults to "0".
|
||||
/// </summary>
|
||||
public string defaultDeviceTier = "0";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this bundle contains an install-time asset pack.
|
||||
/// </summary>
|
||||
public bool containsInstallTimeAssetPack;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum Android SDK version, e.g. from PlayerSettings.Android.
|
||||
/// </summary>
|
||||
public AndroidSdkVersions minSdkVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Options for overriding the default file compression policies.
|
||||
/// </summary>
|
||||
public CompressionOptions compressionOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring asset only app bundles.
|
||||
/// </summary>
|
||||
public AssetOnlyOptions assetOnlyOptions;
|
||||
}
|
||||
|
||||
// Paths where the bundletool jar may potentially be found.
|
||||
private const string PackagePath = "com.google.android.appbundle/Editor/Tools/bundletool-all.jar";
|
||||
private const string PackagesPath = "Packages/" + PackagePath;
|
||||
private const string PluginPath = "Assets/GooglePlayPlugins/" + PackagePath;
|
||||
private const string OverridePath = "bundletool-all.jar";
|
||||
|
||||
/// <summary>
|
||||
/// List of glob patterns specifying files that Unity requires be left uncompressed.
|
||||
/// Similar to PlaybackEngines/AndroidPlayer/Tools/GradleTemplates/mainTemplate.gradle
|
||||
/// </summary>
|
||||
private static readonly string[] UnityUncompressedGlob =
|
||||
{ "assets/**/*.unity3d", "assets/**/*.ress", "assets/**/*.resource" };
|
||||
|
||||
/// <summary>
|
||||
/// Make the Bundle Config exported as JSON cleaner by removing the suffix stripping fields
|
||||
/// that are not enabled.
|
||||
/// This also fix an issue with bundletool v0.11.0 which does not consider the config as valid if
|
||||
/// a suffix stripping field is defined (even if not enabled) for any dimension other than texture compression
|
||||
/// format.
|
||||
/// </summary>
|
||||
/// <param name="configJson">The configuration serialized to a JSON string.</param>
|
||||
/// <returns>The cleaned JSON string.</returns>
|
||||
public static string CleanDisabledSuffixStripping(string configJson)
|
||||
{
|
||||
return configJson.Replace(",\"suffixStripping\":{\"enabled\":false,\"defaultSuffix\":\"\"}", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BundleTool config optimized for Unity-based apps.
|
||||
/// </summary>
|
||||
public static BundletoolConfig.Config MakeConfig(
|
||||
BuildBundleConfigParams configParams, string streamingAssetsPath)
|
||||
{
|
||||
var config = new BundletoolConfig.Config();
|
||||
|
||||
// APK download size is smaller when native libraries are uncompressed. uncompressNativeLibraries sets
|
||||
// android:extractNativeLibs="false" in the manifest, which also reduces on-disk for Android 6.0+ devices.
|
||||
config.optimizations.uncompressNativeLibraries.enabled = true;
|
||||
config.compression.uncompressedGlob.AddRange(UnityUncompressedGlob);
|
||||
|
||||
var compressionOptions = configParams.compressionOptions;
|
||||
if (compressionOptions.UncompressedGlobs != null)
|
||||
{
|
||||
config.compression.uncompressedGlob.AddRange(compressionOptions.UncompressedGlobs);
|
||||
}
|
||||
|
||||
if (!compressionOptions.CompressStreamingAssets)
|
||||
{
|
||||
config.compression.uncompressedGlob.AddRange(GetStreamingAssetsFileGlobs(streamingAssetsPath));
|
||||
}
|
||||
|
||||
if (compressionOptions.CompressInstallTimeAssetPacks)
|
||||
{
|
||||
config.compression.installTimeAssetModuleDefaultCompression = BundletoolConfig.Compressed;
|
||||
}
|
||||
|
||||
var dimensions = config.optimizations.splitsConfig.splitDimension;
|
||||
// Split on ABI so only one set of native libraries (armeabi-v7a, arm64-v8a, or x86) is sent to a device.
|
||||
dimensions.Add(new BundletoolConfig.SplitDimension { value = BundletoolConfig.Abi, negate = false });
|
||||
// Do not split on LANGUAGE since Unity games don't store localized strings in the typical Android manner.
|
||||
dimensions.Add(new BundletoolConfig.SplitDimension { value = BundletoolConfig.Language, negate = true });
|
||||
// Do not split on SCREEN_DENSITY since Unity games don't have per-density resources other than app icons.
|
||||
dimensions.Add(
|
||||
new BundletoolConfig.SplitDimension { value = BundletoolConfig.ScreenDensity, negate = true });
|
||||
if (configParams.enableTcfTargeting)
|
||||
{
|
||||
dimensions.Add(new BundletoolConfig.SplitDimension
|
||||
{
|
||||
value = BundletoolConfig.TextureCompressionFormat,
|
||||
negate = false,
|
||||
suffixStripping =
|
||||
{
|
||||
enabled = true,
|
||||
defaultSuffix = configParams.defaultTcfSuffix
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (configParams.enableDeviceTierTargeting)
|
||||
{
|
||||
dimensions.Add(new BundletoolConfig.SplitDimension
|
||||
{
|
||||
value = BundletoolConfig.DeviceTier,
|
||||
negate = false,
|
||||
suffixStripping =
|
||||
{
|
||||
enabled = true,
|
||||
defaultSuffix = configParams.defaultDeviceTier
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (configParams.assetOnlyOptions != null)
|
||||
{
|
||||
config.type = BundletoolConfig.AssetOnly;
|
||||
config.asset_modules_config = new BundletoolConfig.AssetModulesConfig
|
||||
{
|
||||
app_version = new List<long>(configParams.assetOnlyOptions.AppVersions),
|
||||
asset_version_tag = configParams.assetOnlyOptions.AssetVersionTag
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
// Bundletool requires the below standaloneConfig when supporting install-time asset packs for pre-Lollipop.
|
||||
if (configParams.containsInstallTimeAssetPack &&
|
||||
TextureTargetingTools.IsSdkVersionPreLollipop(configParams.minSdkVersion))
|
||||
{
|
||||
config.optimizations.standaloneConfig.splitDimension.Add(new BundletoolConfig.SplitDimension
|
||||
{ value = BundletoolConfig.Abi, negate = true });
|
||||
|
||||
config.optimizations.standaloneConfig.splitDimension.Add(new BundletoolConfig.SplitDimension
|
||||
{ value = BundletoolConfig.Language, negate = true });
|
||||
|
||||
config.optimizations.standaloneConfig.splitDimension.Add(new BundletoolConfig.SplitDimension
|
||||
{ value = BundletoolConfig.ScreenDensity, negate = true });
|
||||
|
||||
config.optimizations.standaloneConfig.splitDimension.Add(new BundletoolConfig.SplitDimension
|
||||
{ value = BundletoolConfig.TextureCompressionFormat, negate = true });
|
||||
|
||||
config.optimizations.standaloneConfig.strip64BitLibraries = true;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the streaming assets path and returns a list of globs that includes the contained files relative to
|
||||
/// their final location within the APK.
|
||||
/// Note: Does not include .meta files.
|
||||
/// Visible for testing.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetStreamingAssetsFileGlobs(string streamingAssetsPath)
|
||||
{
|
||||
if (!Directory.Exists(streamingAssetsPath))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var streamingAssets = new DirectoryInfo(streamingAssetsPath);
|
||||
|
||||
// Create a glob for every subdirectory in the streaming assets path.
|
||||
// This is more efficient than returning every file from each of the subdirectories.
|
||||
var directoryGlobs = streamingAssets.GetDirectories("*", SearchOption.TopDirectoryOnly)
|
||||
.Select(directory => Path.Combine(directory.FullName, "**"));
|
||||
|
||||
// Create a list of files that are located in the root of the streaming assets directory.
|
||||
var fileNames = streamingAssets.GetFiles("*", SearchOption.TopDirectoryOnly)
|
||||
.Where(file => !file.Name.EndsWith(".meta"))
|
||||
.Select(file => file.FullName);
|
||||
|
||||
// Combine the directory glob list and file list and update the paths to be relative to the final file
|
||||
// locations within the APK.
|
||||
return directoryGlobs.Concat(fileNames)
|
||||
.Select(fullName => "assets/" + fullName.Remove(0, streamingAssetsPath.Length + 1))
|
||||
.Select(name => name.Replace("\\", "/")); // Support Windows' path separator.
|
||||
}
|
||||
|
||||
|
||||
private readonly JavaUtils _javaUtils;
|
||||
|
||||
private string _streamingAssetsPath;
|
||||
private string _bundletoolJarPath;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public BundletoolHelper(JavaUtils javaUtils)
|
||||
{
|
||||
_javaUtils = javaUtils;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (!_javaUtils.Initialize(buildToolLogger))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_streamingAssetsPath = Application.streamingAssetsPath;
|
||||
_bundletoolJarPath = BundletoolJarPath;
|
||||
if (_bundletoolJarPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog("Failed to locate bundletool.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an Android App Bundle at the specified location, overwriting an existing file if necessary. The
|
||||
/// bundle will contain the specified modules, with optional targeting done by texture compression format.
|
||||
/// </summary>
|
||||
/// <param name="outputFile">The output Android App Bundle (AAB) file.</param>
|
||||
/// <param name="moduleFiles">The modules to build inside the bundle.</param>
|
||||
/// <param name="metadataFiles">Metadata files to include in the bundle.</param>
|
||||
/// <param name="buildBundleConfigParams">Contains parameters needed to generate JSON for --config.</param>
|
||||
/// <returns>An error message if there was a problem running bundletool, or null if successful.</returns>
|
||||
public virtual string BuildBundle(string outputFile, IEnumerable<string> moduleFiles,
|
||||
IEnumerable<string> metadataFiles, BuildBundleConfigParams buildBundleConfigParams)
|
||||
{
|
||||
var bundleConfigJsonFile = Path.Combine(Path.GetTempPath(), "BundleConfig.json");
|
||||
var bundleConfig = MakeConfig(buildBundleConfigParams, _streamingAssetsPath);
|
||||
var bundleConfigJsonText = CleanDisabledSuffixStripping(JsonUtility.ToJson(bundleConfig));
|
||||
File.WriteAllText(bundleConfigJsonFile, bundleConfigJsonText);
|
||||
|
||||
var metadataArgumentBuilder = new StringBuilder();
|
||||
foreach (var metadataFile in metadataFiles)
|
||||
{
|
||||
metadataArgumentBuilder.AppendFormat(" --metadata-file={0}", metadataFile);
|
||||
}
|
||||
|
||||
// TODO(b/128882014): fix bundletool support for quoted paths around moduleFiles.
|
||||
return Run(
|
||||
"build-bundle --overwrite --config={0} --modules={1} --output={2}{3}",
|
||||
CommandLine.QuotePath(bundleConfigJsonFile),
|
||||
string.Join(",", moduleFiles.ToArray()),
|
||||
CommandLine.QuotePath(outputFile),
|
||||
metadataArgumentBuilder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an APK Set file from the specified Android App Bundle file.
|
||||
/// </summary>
|
||||
/// <param name="bundleFile">The output file from <see cref="BuildBundle"/>.</param>
|
||||
/// <param name="apkSetFile">A ZIP file containing APKs.</param>
|
||||
/// <param name="buildMode">The type of APKs to produce, such as "persistent" or "instant".</param>
|
||||
/// <param name="enableLocalTesting">
|
||||
/// Whether or not the --local-testing flag is enabled. This will change the behaviour of Play Asset Delivery so
|
||||
/// that fast-follow and on-demand packs are fetched from storage rather than downloaded.
|
||||
/// </param>
|
||||
/// <returns>An error message if there was a problem running bundletool, or null if successful.</returns>
|
||||
public virtual string BuildApkSet(
|
||||
string bundleFile, string apkSetFile, BundletoolBuildMode buildMode, bool enableLocalTesting)
|
||||
{
|
||||
return Run(
|
||||
"build-apks --bundle={0} --output={1} --mode={2}{3}",
|
||||
CommandLine.QuotePath(bundleFile),
|
||||
CommandLine.QuotePath(apkSetFile),
|
||||
buildMode.GetModeFlag(),
|
||||
enableLocalTesting ? " --local-testing" : "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the specified APK Set file to device.
|
||||
/// </summary>
|
||||
/// <param name="apkSetFile">A ZIP file containing APKs, produced from "bundletool build-apks".</param>
|
||||
/// <param name="adbPath">Path to the adb executable used to install apks.</param>
|
||||
/// <returns>An error message if there was a problem running bundletool, or null if successful.</returns>
|
||||
public virtual string InstallApkSet(string apkSetFile, string adbPath)
|
||||
{
|
||||
return Run("install-apks --adb={0} --apks={1}",
|
||||
CommandLine.QuotePath(adbPath),
|
||||
CommandLine.QuotePath(apkSetFile));
|
||||
}
|
||||
|
||||
private string Run(string bundletoolCommand, params object[] args)
|
||||
{
|
||||
var bundletoolCommandWithArgs = string.Format(bundletoolCommand, args);
|
||||
var arguments =
|
||||
string.Format("-jar {0} {1}", CommandLine.QuotePath(_bundletoolJarPath), bundletoolCommandWithArgs);
|
||||
var result = CommandLine.Run(_javaUtils.JavaBinaryPath, arguments);
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the absolute path to the bundletool jar packaged with the plugin.
|
||||
/// Returning the absolute path handles cases where Unity treats relative paths
|
||||
/// differently than the command line. For example Unity treats the Packages/
|
||||
/// folder as a symlink to Library/PackageCache while the command line does not.
|
||||
/// </summary>
|
||||
private static string BundletoolJarPath
|
||||
{
|
||||
get
|
||||
{
|
||||
// GUIDToAssetPath throws an exception if called on a non-main thread. To catch this case early, we
|
||||
// define this variable here, rather than below, where it is used.
|
||||
var guidPath = AssetDatabase.GUIDToAssetPath("c52291e63505c4121a167e6f0121c1b1");
|
||||
|
||||
string relativePath = null;
|
||||
|
||||
// Bundletool is normally included with the .unitypackage or UPM package, but the bundletool used for
|
||||
// builds can be overridden by placing a JAR in the project root, i.e. the parent folder of "Assets".
|
||||
if (File.Exists(OverridePath))
|
||||
{
|
||||
relativePath = OverridePath;
|
||||
}
|
||||
else if (File.Exists(PackagesPath))
|
||||
{
|
||||
relativePath = PackagesPath;
|
||||
}
|
||||
else if (File.Exists(PluginPath))
|
||||
{
|
||||
relativePath = PluginPath;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(guidPath) && guidPath.EndsWith(".jar") && File.Exists(guidPath))
|
||||
{
|
||||
relativePath = guidPath;
|
||||
}
|
||||
|
||||
return relativePath == null ? null : Path.GetFullPath(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc0e37b8322b54a4da00fdca472c4bb8
|
||||
timeCreated: 1556913938
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// A tool used during app builds.
|
||||
/// </summary>
|
||||
public interface IBuildTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Method that should be called to initialize the <see cref="IBuildTool"/> for use and to check whether there
|
||||
/// are any issues with the tool or its dependencies. This method should only be run on the main thread.
|
||||
/// Since most <see cref="IBuildTool"/> work may be done on background threads, use this method to copy any
|
||||
/// Unity state that can only be accessed from the main thread, e.g. properties from Unity's Application,
|
||||
/// EditorPrefs, or PlayerSettings classes.
|
||||
/// </summary>
|
||||
/// <param name="buildToolLogger">Used to log errors and/or display dialogs if there is a build issue.</param>
|
||||
/// <returns>true if this tool is ready to be used, or false if there is an error.</returns>
|
||||
bool Initialize(BuildToolLogger buildToolLogger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ed1ed129773a49ad9caa331244333ff
|
||||
timeCreated: 1556899571
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2020 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
using Google.Android.AppBundle.Editor.Internal.Utils;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods that call the JDK tool "jarsigner" to sign an Android App Bundle.
|
||||
/// </summary>
|
||||
public class JarSigner : IBuildTool
|
||||
{
|
||||
private readonly JavaUtils _javaUtils;
|
||||
|
||||
private bool _useCustomKeystore;
|
||||
private string _keystoreName;
|
||||
private string _keystorePass;
|
||||
private string _keyaliasName;
|
||||
private string _keyaliasPass;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public JarSigner(JavaUtils javaUtils)
|
||||
{
|
||||
_javaUtils = javaUtils;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (!_javaUtils.Initialize(buildToolLogger))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_javaUtils.JarSignerBinaryPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Unable to locate jarsigner. Check that a recent version of the JDK " +
|
||||
"is installed and check the Console log for more details on the error.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cache PlayerSettings on the main thread so they can be accessed from a background thread.
|
||||
_keystoreName = PlayerSettings.Android.keystoreName;
|
||||
_keystorePass = PlayerSettings.Android.keystorePass;
|
||||
_keyaliasName = PlayerSettings.Android.keyaliasName;
|
||||
_keyaliasPass = PlayerSettings.Android.keyaliasPass;
|
||||
#if UNITY_2019_1_OR_NEWER
|
||||
_useCustomKeystore = PlayerSettings.Android.useCustomKeystore;
|
||||
#else
|
||||
_useCustomKeystore = !string.IsNullOrEmpty(_keystoreName) && !string.IsNullOrEmpty(_keyaliasName);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the Android Player Settings have a Custom Keystore configured, false otherwise.
|
||||
/// </summary>
|
||||
public bool UseCustomKeystore
|
||||
{
|
||||
get { return _useCustomKeystore; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously calls the jarsigner tool to sign the specified ZIP file using a custom keystore.
|
||||
/// This can be used to sign Android App Bundles.
|
||||
/// </summary>
|
||||
/// <returns>An error message if there was a problem running jarsigner, or null if successful.</returns>
|
||||
public string Sign(string zipFilePath)
|
||||
{
|
||||
if (!_useCustomKeystore)
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected request to sign without a custom keystore");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_keystoreName))
|
||||
{
|
||||
return "Unable to sign since the keystore file path is unspecified";
|
||||
}
|
||||
|
||||
if (!File.Exists(_keystoreName))
|
||||
{
|
||||
return string.Format("Failed to locate keystore file: {0}", _keystoreName);
|
||||
}
|
||||
|
||||
var arguments = string.Format(
|
||||
"-J-Duser.language=en -keystore {0} {1} {2}",
|
||||
CommandLine.QuotePath(_keystoreName),
|
||||
CommandLine.QuotePath(zipFilePath),
|
||||
CommandLine.QuotePath(_keyaliasName));
|
||||
|
||||
var promptToPasswordDictionary = new Dictionary<string, string>
|
||||
{
|
||||
{"Enter Passphrase for keystore:", _keystorePass},
|
||||
// Example keyalias password prompt: "Enter key password for myalias:"
|
||||
{"Enter key password for .+:", _keyaliasPass}
|
||||
};
|
||||
var responder = new JarSignerResponder(promptToPasswordDictionary);
|
||||
var result = CommandLine.Run(_javaUtils.JarSignerBinaryPath, arguments, ioHandler: responder.AggregateLine);
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks jarsigner's stderr for password prompts and provides the associated password to jarsigner's stdin.
|
||||
/// This is more secure than providing passwords on the command line (where passwords are visible to process
|
||||
/// listing tools like "ps") or using file-based password input (where passwords are written to disk).
|
||||
/// </summary>
|
||||
private class JarSignerResponder : CommandLine.LineReader
|
||||
{
|
||||
private readonly Dictionary<Regex, string> _promptToPasswordDictionary;
|
||||
|
||||
public JarSignerResponder(Dictionary<string, string> promptToPasswordDictionary)
|
||||
{
|
||||
_promptToPasswordDictionary =
|
||||
promptToPasswordDictionary.ToDictionary(
|
||||
kvp => RegexHelper.CreateCompiled(kvp.Key), kvp => kvp.Value);
|
||||
LineHandler += CheckAndRespond;
|
||||
}
|
||||
|
||||
private void CheckAndRespond(Process process, StreamWriter stdin, CommandLine.StreamData data)
|
||||
{
|
||||
if (process.HasExited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The password prompt text won't have a trailing newline, so read ahead on stdout to locate it.
|
||||
var stderrData = GetBufferedData(CommandLine.StandardErrorStreamDataHandle);
|
||||
var stderrText = Aggregate(stderrData).text;
|
||||
var password = _promptToPasswordDictionary
|
||||
.Where(kvp => kvp.Key.IsMatch(stderrText))
|
||||
.Select(kvp => kvp.Value)
|
||||
.FirstOrDefault();
|
||||
if (password == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Flush();
|
||||
// Use UTF8 to support non ASCII passwords
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password + Environment.NewLine);
|
||||
stdin.BaseStream.Write(passwordBytes, 0, passwordBytes.Length);
|
||||
stdin.BaseStream.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf519927705149098aece6a0c73137fe
|
||||
timeCreated: 1594212511
|
||||
@@ -0,0 +1,217 @@
|
||||
// Copyright 2019 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.IO;
|
||||
using Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Build tool for finding the path to Java executables.
|
||||
/// </summary>
|
||||
public class JavaUtils : IBuildTool
|
||||
{
|
||||
private bool _isInitialized;
|
||||
private string _javaBinaryPath;
|
||||
private string _jarBinaryPath;
|
||||
private string _jarSignerBinaryPath;
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var jdkPath = GetJdkPath();
|
||||
if (jdkPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate the JDK. Check Preferences -> External Tools to set the JDK path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_javaBinaryPath = GetBinaryPath(jdkPath, "java");
|
||||
if (_javaBinaryPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate Java. Check Preferences -> External Tools to set the JDK path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_jarBinaryPath = GetBinaryPath(jdkPath, "jar");
|
||||
if (_jarBinaryPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate Java's jar command. Check Preferences -> External Tools to set the JDK path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_jarSignerBinaryPath = GetBinaryPath(jdkPath, "jarsigner");
|
||||
if (_jarSignerBinaryPath == null)
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate Java's jarsigner command. Check Preferences -> External Tools to set the JDK path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to the java executable.
|
||||
/// </summary>
|
||||
public virtual string JavaBinaryPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_javaBinaryPath == null)
|
||||
{
|
||||
throw new BuildToolNotInitializedException(this);
|
||||
}
|
||||
|
||||
return _javaBinaryPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to the jar executable.
|
||||
/// </summary>
|
||||
public virtual string JarBinaryPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_jarBinaryPath == null)
|
||||
{
|
||||
throw new BuildToolNotInitializedException(this);
|
||||
}
|
||||
|
||||
return _jarBinaryPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to the jarsigner executable.
|
||||
/// </summary>
|
||||
public virtual string JarSignerBinaryPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_jarSignerBinaryPath == null)
|
||||
{
|
||||
throw new BuildToolNotInitializedException(this);
|
||||
}
|
||||
|
||||
return _jarSignerBinaryPath;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetBinaryPath(string jdkPath, string toolName)
|
||||
{
|
||||
var toolPath = Path.Combine(jdkPath, Path.Combine("bin", toolName + CommandLine.GetExecutableExtension()));
|
||||
if (File.Exists(toolPath))
|
||||
{
|
||||
return toolPath;
|
||||
}
|
||||
|
||||
toolPath = CommandLine.FindExecutable(toolName);
|
||||
return File.Exists(toolPath) ? toolPath : null;
|
||||
}
|
||||
|
||||
private static string GetJdkPath()
|
||||
{
|
||||
#if UNITY_2019_3_OR_NEWER && UNITY_ANDROID
|
||||
var toolsJdkPath = UnityEditor.Android.AndroidExternalToolsSettings.jdkRootPath;
|
||||
if (!string.IsNullOrEmpty(toolsJdkPath) && Directory.Exists(toolsJdkPath))
|
||||
{
|
||||
Debug.LogFormat("Using tools JDK path: {0}", toolsJdkPath);
|
||||
return toolsJdkPath;
|
||||
}
|
||||
#endif
|
||||
var embeddedJdkPath = GetEmbeddedJdkPath();
|
||||
if (!string.IsNullOrEmpty(embeddedJdkPath) && Directory.Exists(embeddedJdkPath))
|
||||
{
|
||||
Debug.LogFormat("Using embedded JDK path: {0}", embeddedJdkPath);
|
||||
return embeddedJdkPath;
|
||||
}
|
||||
|
||||
var preferencesJdkPath = EditorPrefs.GetString("JdkPath");
|
||||
if (!string.IsNullOrEmpty(preferencesJdkPath) && Directory.Exists(preferencesJdkPath))
|
||||
{
|
||||
Debug.LogFormat("Using preferences JDK path: {0}", preferencesJdkPath);
|
||||
return preferencesJdkPath;
|
||||
}
|
||||
|
||||
var environmentJdkPath = Environment.GetEnvironmentVariable("JAVA_HOME");
|
||||
if (!string.IsNullOrEmpty(environmentJdkPath) && Directory.Exists(environmentJdkPath))
|
||||
{
|
||||
Debug.LogFormat("Using environment JDK path: {0}", environmentJdkPath);
|
||||
return environmentJdkPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the path to Unity's embedded version of OpenJDK, or null if not enabled.
|
||||
/// Unity 2018.3 added the option to use a version of OpenJDK that is installed with Unity.
|
||||
/// </summary>
|
||||
private static string GetEmbeddedJdkPath()
|
||||
{
|
||||
// Users switching between Unity versions may have JdkUseEmbedded present in their preferences even though
|
||||
// their current version of Unity does not support it. So we return null for other versions to avoid that
|
||||
// case.
|
||||
#if !UNITY_2018_3_OR_NEWER || UNITY_2019_3_OR_NEWER
|
||||
return null;
|
||||
#else
|
||||
// For Unity 2018.3 through 2019.2 construct the embedded JDK path as follows.
|
||||
if (EditorPrefs.GetInt("JdkUseEmbedded") != 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string pathRelativeToEditor;
|
||||
switch (Application.platform)
|
||||
{
|
||||
case RuntimePlatform.LinuxEditor:
|
||||
pathRelativeToEditor = "Data/PlaybackEngines/AndroidPlayer/Tools/OpenJdk/Linux/";
|
||||
break;
|
||||
case RuntimePlatform.OSXEditor:
|
||||
pathRelativeToEditor = "PlaybackEngines/AndroidPlayer/Tools/OpenJdk/MacOS/";
|
||||
break;
|
||||
case RuntimePlatform.WindowsEditor:
|
||||
pathRelativeToEditor = "Data/PlaybackEngines/AndroidPlayer/Tools/OpenJdk/Windows/";
|
||||
break;
|
||||
default:
|
||||
throw new Exception(
|
||||
"Cannot get embedded JDK path for unsupported platform: " + Application.platform);
|
||||
}
|
||||
|
||||
var unityEditor = new FileInfo(EditorApplication.applicationPath);
|
||||
if (unityEditor.DirectoryName == null)
|
||||
{
|
||||
throw new Exception(
|
||||
"Cannot get embedded JDK path. EditorApplication.applicationPath returned an unexpected value: " +
|
||||
EditorApplication.applicationPath);
|
||||
}
|
||||
|
||||
return Path.Combine(unityEditor.DirectoryName, pathRelativeToEditor);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16791a24cc9874bc6a41407b887146f5
|
||||
timeCreated: 1556899571
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
#if !UNITY_2018_3_OR_NEWER
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a task after a player build causes the application domain to reset and scripts to be reloaded.
|
||||
/// Starting with Unity 2018.3 player builds no longer reset the application domain.
|
||||
/// </summary>
|
||||
public static class PostBuildRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that provides an abstraction layer above <see cref="PostBuildTask"/> in order to capture
|
||||
/// the task's full type name, which is needed for task deserialization.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
private class TaskConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Location of the <see cref="PostBuildTask"/> serialized file.
|
||||
/// </summary>
|
||||
public string taskFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// The full type name of the class that was serialized at <see cref="taskFilePath"/>.
|
||||
/// </summary>
|
||||
public string taskTypeName;
|
||||
}
|
||||
|
||||
private static readonly string PostBuildTaskConfigFilePath =
|
||||
Path.Combine("Library", "PlayPostBuildTaskConfig.json");
|
||||
|
||||
private static float _progress;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified task to disk for running after scripts are reloaded.
|
||||
/// </summary>
|
||||
public static void RunTask(PostBuildTask task)
|
||||
{
|
||||
if (File.Exists(PostBuildTaskConfigFilePath))
|
||||
{
|
||||
Debug.LogWarningFormat(
|
||||
"Creating a new post-build task when one already exists: {0}", PostBuildTaskConfigFilePath);
|
||||
}
|
||||
|
||||
var taskFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
var taskConfig = new TaskConfig {taskFilePath = taskFilePath, taskTypeName = task.GetType().FullName};
|
||||
|
||||
SerializeToJson(taskFilePath, task);
|
||||
SerializeToJson(PostBuildTaskConfigFilePath, taskConfig);
|
||||
|
||||
_progress = 0.0f;
|
||||
// We don't unsubscribe from updates (except when canceling) because script reload will reset everything.
|
||||
EditorApplication.update += HandleUpdate;
|
||||
}
|
||||
|
||||
[DidReloadScripts]
|
||||
private static void DidReloadScripts()
|
||||
{
|
||||
if (!File.Exists(PostBuildTaskConfigFilePath))
|
||||
{
|
||||
// In the common case the scripts reloaded because of a script change, etc. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
PostBuildTask postBuildTask;
|
||||
try
|
||||
{
|
||||
// Hide the progress bar that was shown in HandleUpdate().
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
Debug.LogFormat("Loading post-build task config file: {0}", PostBuildTaskConfigFilePath);
|
||||
var taskConfigJsonText = File.ReadAllText(PostBuildTaskConfigFilePath);
|
||||
var taskConfig = JsonUtility.FromJson<TaskConfig>(taskConfigJsonText);
|
||||
var taskJsonText = File.ReadAllText(taskConfig.taskFilePath);
|
||||
var taskTypeName = taskConfig.taskTypeName;
|
||||
|
||||
Debug.LogFormat("Loading and running post-build task for type: {0}", taskTypeName);
|
||||
var postBuildTaskObj = JsonUtility.FromJson(taskJsonText, Type.GetType(taskTypeName));
|
||||
postBuildTask = (PostBuildTask) postBuildTaskObj;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove the config file so that this task doesn't run again after the next application domain reset.
|
||||
File.Delete(PostBuildTaskConfigFilePath);
|
||||
}
|
||||
|
||||
postBuildTask.RunPostBuildTask();
|
||||
}
|
||||
|
||||
// Handler for EditorApplication.update callbacks.
|
||||
private static void HandleUpdate()
|
||||
{
|
||||
// Magic number to partially fill up the progress bar during the time that it takes scripts to reload.
|
||||
// Note that the best number varies by version, but better to finish with the progress bar less than
|
||||
// full on some versions than full long before scripts reload on other versions.
|
||||
_progress += 0.0005f;
|
||||
|
||||
if (!EditorUtility.DisplayCancelableProgressBar("Waiting for scripts to reload...", null, _progress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("Canceling post-build task before scripts reload...");
|
||||
// Since the existence of the config file is the signal to run a post-build task, delete the file to cancel.
|
||||
File.Delete(PostBuildTaskConfigFilePath);
|
||||
EditorUtility.ClearProgressBar();
|
||||
EditorApplication.update -= HandleUpdate;
|
||||
}
|
||||
|
||||
private static void SerializeToJson(string path, object obj)
|
||||
{
|
||||
var jsonText = JsonUtility.ToJson(obj);
|
||||
File.WriteAllText(path, jsonText);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70edb5163b984f9799e762ef659dabe6
|
||||
timeCreated: 1557153210
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019 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;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract serializable class representing a task to be run after scripts reload following a player build.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class PostBuildTask
|
||||
{
|
||||
/// <summary>
|
||||
/// A method that is run on the main thread after scripts are reloaded.
|
||||
/// </summary>
|
||||
public abstract void RunPostBuildTask();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e6ec7abd981a4580b1ec0a21602c072
|
||||
timeCreated: 1558031253
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2019 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.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Build tool for determining whether this is a release build and displaying any relevant errors if so.
|
||||
/// </summary>
|
||||
public class ReleaseBuildHelper : IBuildTool
|
||||
{
|
||||
private const string Arm64RequiredDescription =
|
||||
"ARM64 libraries must be included in any APK or Android App Bundle published on Play.";
|
||||
|
||||
private const string Il2CppRequiredDescription =
|
||||
"\n\nNote: the IL2CPP Scripting Backend is required to build ARM64 libraries.";
|
||||
|
||||
private readonly JarSigner _jarSigner;
|
||||
|
||||
public ReleaseBuildHelper(JarSigner jarSigner)
|
||||
{
|
||||
_jarSigner = jarSigner;
|
||||
}
|
||||
|
||||
// TODO(b/130759565): add check for PlayerSettings.productName
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
// Try to determine whether this is intended to be a release build.
|
||||
if (!_jarSigner.UseCustomKeystore || EditorUserBuildSettings.development)
|
||||
{
|
||||
// Seems like a debug build, so no need to do release build checks.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasIconForTargetGroup(BuildTargetGroup.Unknown) && !HasIconForTargetGroup(BuildTargetGroup.Android))
|
||||
{
|
||||
buildToolLogger.DisplayErrorDialog(
|
||||
"Failed to locate a Default Icon or an Android Icon for this project. " +
|
||||
"Check Player Settings to set an icon");
|
||||
return false;
|
||||
}
|
||||
|
||||
string message;
|
||||
switch (AndroidArchitectureHelper.ArchitectureStatus)
|
||||
{
|
||||
case AndroidArchitectureHelper.Status.Ok:
|
||||
return true;
|
||||
case AndroidArchitectureHelper.Status.ArmV7Disabled:
|
||||
message = "ARMv7 and " + Arm64RequiredDescription + Il2CppRequiredDescription;
|
||||
break;
|
||||
case AndroidArchitectureHelper.Status.Il2CppDisabled:
|
||||
message = Arm64RequiredDescription + Il2CppRequiredDescription;
|
||||
break;
|
||||
case AndroidArchitectureHelper.Status.Arm64Disabled:
|
||||
message = Arm64RequiredDescription;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
message += "\n\nClick \"OK\" to enable an IL2CPP build for ARMv7 and ARM64 architectures.";
|
||||
if (buildToolLogger.DisplayActionableErrorDialog(message))
|
||||
{
|
||||
AndroidArchitectureHelper.FixTargetArchitectures();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasIconForTargetGroup(BuildTargetGroup buildTargetGroup)
|
||||
{
|
||||
var icons = PlayerSettings.GetIconsForTargetGroup(buildTargetGroup);
|
||||
return icons != null && icons.Any(icon => icon != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a72ad9ee7052447e385d166772c9b8ac
|
||||
timeCreated: 1556921099
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 Google.Android.AppBundle.Editor.Internal.PlayServices;
|
||||
|
||||
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods that use Java's "jar" command for zip creation/extraction since zip functionality
|
||||
/// isn't built into .NET 3.5.
|
||||
/// </summary>
|
||||
public class ZipUtils : IBuildTool
|
||||
{
|
||||
private readonly JavaUtils _javaUtils;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public ZipUtils(JavaUtils javaUtils)
|
||||
{
|
||||
_javaUtils = javaUtils;
|
||||
}
|
||||
|
||||
public virtual bool Initialize(BuildToolLogger buildToolLogger)
|
||||
{
|
||||
return _javaUtils.Initialize(buildToolLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ZIP file containing the specified file in the specified directory.
|
||||
/// </summary>
|
||||
/// <returns>null if the operation succeeded, or an error message if it failed.</returns>
|
||||
public virtual string CreateZipFile(string zipFilePath, string inputDirectoryName, string inputFileName)
|
||||
{
|
||||
if (inputFileName.Contains(" "))
|
||||
{
|
||||
throw new ArgumentException("Spaces are not supported for inputFileName.", "inputFileName");
|
||||
}
|
||||
|
||||
// On windows, we can't support a directory with spaces, because jar does not support quotes around the
|
||||
// inputDirectoryName.
|
||||
#if UNITY_EDITOR_WIN
|
||||
if (inputDirectoryName.Contains(" "))
|
||||
{
|
||||
throw new ArgumentException("Spaces are not supported for inputDirectoryName.", "inputDirectoryName");
|
||||
}
|
||||
#else
|
||||
inputDirectoryName = CommandLine.QuotePath(inputDirectoryName);
|
||||
#endif
|
||||
|
||||
// Create zip file with options "0" (no per-file compression) and "M" (no JAR manifest file).
|
||||
var arguments = string.Format(
|
||||
"c0Mf {0} -C {1} {2}",
|
||||
CommandLine.QuotePath(zipFilePath),
|
||||
inputDirectoryName,
|
||||
inputFileName);
|
||||
var result = CommandLine.Run(_javaUtils.JarBinaryPath, arguments);
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unzips the specified ZIP file into the specified output location.
|
||||
/// </summary>
|
||||
/// <returns>null if the operation succeeded, or an error message if it failed.</returns>
|
||||
public virtual string UnzipFile(string zipFilePath, string outputDirectoryName)
|
||||
{
|
||||
var arguments = string.Format("xf {0}", CommandLine.QuotePath(zipFilePath));
|
||||
var result = CommandLine.Run(_javaUtils.JarBinaryPath, arguments, outputDirectoryName);
|
||||
return result.exitCode == 0 ? null : result.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bed5ac126295a479887b763b3778752f
|
||||
timeCreated: 1556919494
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user