chunk 2: remaining non-audio non-NewImport assets

This commit is contained in:
2026-04-06 11:25:19 +03:00
parent 0d11a097d8
commit ed675b9f4a
3742 changed files with 1640416 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
// 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 UnityEditor;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Provides utilities for checking whether this version of Unity supports building ARM64 libraries.
/// </summary>
public static class AndroidArchitectureHelper
{
/// <summary>
/// Indicates the current status of architecture-related build settings.
/// </summary>
public enum Status
{
/// <summary>
/// No issues with selected architectures.
/// </summary>
Ok,
/// <summary>
/// ARMv7 is disabled.
/// </summary>
ArmV7Disabled,
/// <summary>
/// IL2CPP is disabled, also implying that ARM64 is unavailable.
/// </summary>
Il2CppDisabled,
/// <summary>
/// IL2CPP is enabled, but ARM64 is disabled.
/// </summary>
Arm64Disabled
}
/// <summary>
/// Returns true if this version of the Unity Editor has native support for building an Android App Bundle,
/// and false otherwise.
/// </summary>
public static Status ArchitectureStatus
{
get
{
if (!IsArchitectureEnabled(AndroidArchitecture.ARMv7))
{
return Status.ArmV7Disabled;
}
if (PlayerSettings.GetScriptingBackend(BuildTargetGroup.Android) != ScriptingImplementation.IL2CPP)
{
return Status.Il2CppDisabled;
}
return IsArchitectureEnabled(AndroidArchitecture.ARM64)
? Status.Ok
: Status.Arm64Disabled;
}
}
/// <summary>
/// Enables an IL2CPP build of ARMv7 and ARM64 without affecting whether X86 is enabled.
/// IL2CPP is required since Unity's Mono scripting backend doesn't support ARM64.
/// </summary>
public static void FixTargetArchitectures()
{
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
PlayerSettings.Android.targetArchitectures |= AndroidArchitecture.ARMv7 | AndroidArchitecture.ARM64;
}
/// <summary>
/// Enable the IL2CPP scripting backend and all Android architectures supported by IL2CPP builds.
/// </summary>
public static void EnableIl2CppBuildArchitectures()
{
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.All;
}
/// <summary>
/// Enable the Mono scripting backend and all Android architectures supported by Mono builds.
/// Note: Unity 2019.4.31+ only supports x86 for IL2CPP builds.
/// </summary>
public static void EnableMonoBuildArchitectures()
{
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.Mono2x);
#if UNITY_2019_3_OR_NEWER
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARMv7;
#else
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARMv7 | AndroidArchitecture.X86;
#endif
}
private static bool IsArchitectureEnabled(AndroidArchitecture androidArchitecture)
{
// Note: Enum.HasFlag() was introduced in .NET 4.x
return (PlayerSettings.Android.targetArchitectures & androidArchitecture) == androidArchitecture;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f33b5abf4e34544e597d9b43a21c8741
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: dac17760a2fd2434188c3671e822fcc2
folderAsset: yes
timeCreated: 1540331103
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
// 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.Xml.Linq;
using Google.Android.AppBundle.Editor.AssetPacks;
namespace Google.Android.AppBundle.Editor.Internal.AndroidManifest
{
/// <summary>
/// A helper class for creating asset pack AndroidManifest xml documents.
/// </summary>
public static class AssetPackManifestHelper
{
/// <summary>
/// Creates a new XDocument representing the AndroidManifest.xml for an asset pack.
/// </summary>
/// <param name="packageName">Package name of this application.</param>
/// <param name="assetPackName">The name of the asset pack.</param>
/// <param name="deliveryMode">The <see cref="AssetPackDeliveryMode"/> of this asset pack.</param>
public static XDocument CreateAssetPackManifestXDocument(
string packageName, string assetPackName, AssetPackDeliveryMode deliveryMode)
{
// TODO(b/129943210): Add support for <dist:instant-delivery>
var deliveryTypeXName = ManifestConstants.DistDeliveryXName;
XName deliveryModeXName;
switch (deliveryMode)
{
case AssetPackDeliveryMode.OnDemand:
deliveryModeXName = ManifestConstants.DistOnDemandXName;
break;
case AssetPackDeliveryMode.InstallTime:
deliveryModeXName = ManifestConstants.DistInstallTimeName;
break;
case AssetPackDeliveryMode.FastFollow:
deliveryModeXName = ManifestConstants.DistFastFollowName;
break;
default:
throw new ArgumentException("Unexpected delivery mode: " + deliveryMode, "deliveryMode");
}
var moduleElement = new XElement(
ManifestConstants.DistModuleXName,
new XAttribute(ManifestConstants.DistTypeXName, ManifestConstants.AssetPack),
new XElement(deliveryTypeXName,
new XElement(deliveryModeXName)),
new XElement(ManifestConstants.DistFusingXName,
new XAttribute(ManifestConstants.DistIncludeXName, ManifestConstants.ValueTrue))
);
return new XDocument(new XElement(
ManifestConstants.Manifest,
new XAttribute(ManifestConstants.AndroidXmlns, XNamespace.Get(ManifestConstants.AndroidNamespaceUrl)),
new XAttribute(ManifestConstants.DistXmlns, XNamespace.Get(ManifestConstants.DistNamespaceUrl)),
new XAttribute(ManifestConstants.Package, packageName),
new XAttribute(ManifestConstants.Split, assetPackName),
moduleElement));
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f3c4c13f33e6f48dc9e8130d699f1428
timeCreated: 1540331105
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
// 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.Xml.Linq;
using Google.Android.AppBundle.Editor.Internal.BuildTools;
namespace Google.Android.AppBundle.Editor.Internal.AndroidManifest
{
/// <summary>
/// Provides an interface to modify the AndroidManifest xml documents configured for asset packs.
/// </summary>
public interface IAssetPackManifestTransformer : IBuildTool
{
/// <summary>
/// Modifies the specified AndroidManifest xml document. Returns an error message if the operation fails and
/// null otherwise.
/// </summary>
string Transform(XDocument document);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 12432c2ade03483e9e956f11ddde2992
timeCreated: 1574716612

View File

@@ -0,0 +1,66 @@
// 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.Xml.Linq;
namespace Google.Android.AppBundle.Editor.Internal.AndroidManifest
{
/// <summary>
/// String constants used by AndroidManifest.xml files.
/// </summary>
public static class ManifestConstants
{
public const string Action = "action";
public const string Activity = "activity";
public const string Application = "application";
public const string AssetPack = "asset-pack";
public const string Category = "category";
public const string Data = "data";
public const string IntentFilter = "intent-filter";
public const string Manifest = "manifest";
public const string Package = "package";
public const string Service = "service";
public const string Split = "split";
public const string ValueTrue = "true";
public const string AndroidNamespaceAlias = "android";
public const string AndroidNamespaceUrl = "http://schemas.android.com/apk/res/android";
public static readonly XName AndroidXmlns = XNamespace.Xmlns + AndroidNamespaceAlias;
public static readonly XName AndroidEnabledXName = XName.Get("enabled", AndroidNamespaceUrl);
public static readonly XName AndroidIconXName = XName.Get("icon", AndroidNamespaceUrl);
public static readonly XName AndroidLabelXName = XName.Get("label", AndroidNamespaceUrl);
public static readonly XName AndroidNameXName = XName.Get("name", AndroidNamespaceUrl);
public static readonly XName AndroidTargetSandboxVersionXName =
XName.Get("targetSandboxVersion", AndroidNamespaceUrl);
public const string DistNamespaceAlias = "dist";
public const string DistNamespaceUrl = "http://schemas.android.com/apk/distribution";
public static readonly XName DistXmlns = XNamespace.Xmlns + DistNamespaceAlias;
public static readonly XName DistDeliveryXName = XName.Get("delivery", DistNamespaceUrl);
public static readonly XName DistFastFollowName = XName.Get("fast-follow", DistNamespaceUrl);
public static readonly XName DistFusingXName = XName.Get("fusing", DistNamespaceUrl);
public static readonly XName DistIncludeXName = XName.Get("include", DistNamespaceUrl);
public static readonly XName DistInstallTimeName = XName.Get("install-time", DistNamespaceUrl);
public static readonly XName DistInstantXName = XName.Get("instant", DistNamespaceUrl);
public static readonly XName DistModuleXName = XName.Get("module", DistNamespaceUrl);
public static readonly XName DistOnDemandXName = XName.Get("on-demand", DistNamespaceUrl);
public static readonly XName DistTypeXName = XName.Get("type", DistNamespaceUrl);
public const string ToolsNamespaceAlias = "tools";
public const string ToolsNamespaceUrl = "http://schemas.android.com/tools";
public static readonly XName ToolsXmlns = XNamespace.Xmlns + ToolsNamespaceAlias;
public static readonly XName ToolsReplaceXName = XName.Get("replace", ToolsNamespaceUrl);
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 16b5ed2898b684f67ab9286244de2e19
timeCreated: 1556563916
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
// 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.AssetPacks;
using UnityEditor;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Provides "Android App Bundle" and Android build related menu items for the Unity Editor.
/// </summary>
public static class AppBundleEditorMenu
{
private const string AppBundleMenuName = GoogleEditorMenu.MainMenuName + "/Android App Bundle/";
private const string RootMenuName = GoogleEditorMenu.MainMenuName + "/";
private const int AboveLine = GoogleEditorMenu.AndroidAppBundlePriority;
private const int BelowLine = GoogleEditorMenu.AndroidAppBundlePriority + GoogleEditorMenu.SeparatorSize;
[MenuItem(AppBundleMenuName + "Asset Delivery Settings...", false, AboveLine)]
private static void ShowAssetDeliveryWindow()
{
AssetDeliveryWindow.ShowWindow();
}
[MenuItem(AppBundleMenuName + GoogleEditorMenu.BuildSettings, false, AboveLine + 1)]
private static void OpenEditorSettings()
{
BuildSettingsWindow.ShowWindow();
}
[MenuItem(AppBundleMenuName + GoogleEditorMenu.ViewDocumentation, false, BelowLine)]
private static void ViewDocumentation()
{
Application.OpenURL("https://developer.android.com/guide/app-bundle/asset-delivery/build-unity");
}
[MenuItem(AppBundleMenuName + GoogleEditorMenu.ViewLicense, false, BelowLine + 1)]
private static void ViewLicense()
{
// The guid is for GooglePlayPlugins/com.google.android.appbundle/LICENSE.md
GoogleEditorMenu.OpenFileByGuid("4b995f494568e47259d526c5211d778b");
}
[MenuItem(AppBundleMenuName + GoogleEditorMenu.FileBug, false, BelowLine + 2)]
private static void ViewPlayPluginsIssuesPage()
{
GoogleEditorMenu.ViewPlayPluginsIssuesPage();
}
[MenuItem(RootMenuName + "Build Android App Bundle...", false, GoogleEditorMenu.RootMenuPriority + 10)]
private static void BuildAndroidAppBundle()
{
AppBundlePublisher.Build();
}
[MenuItem(RootMenuName + "Build and Run #%r", false, GoogleEditorMenu.RootMenuPriority + 11)]
private static void BuildAndRun()
{
BuildAndRunner.BuildAndRun();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79f87642d146d41d7bdaaa2328b529b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,310 @@
// 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.AssetPacks;
using Google.Android.AppBundle.Editor.Internal.BuildTools;
using UnityEditor;
#if !UNITY_2018_3_OR_NEWER
using Google.Android.AppBundle.Editor.Internal.Config;
#endif
#if UNITY_2018_2_OR_NEWER
using UnityEngine;
#endif
#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
using System.Threading.Tasks;
#endif
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Helper to build <a href="https://developer.android.com/platform/technology/app-bundle/">Android App Bundles</a>.
/// </summary>
public static class AppBundlePublisher
{
private class AppBundleBuildSettings
{
public BuildPlayerOptions buildPlayerOptions;
public AssetPackConfig assetPackConfig;
public bool runOnDevice;
public bool forceSynchronousBuild;
}
#if !UNITY_2018_3_OR_NEWER
/// <summary>
/// A <see cref="PostBuildTask"/> that serializes state to resume an app bundle build after a script reload
/// occurs. Starting with Unity 2018.3 player builds no longer reset the application domain.
/// </summary>
[Serializable]
private class AppBundlePostBuildTask : PostBuildTask
{
public SerializableAssetPackConfig serializableAssetPackConfig;
public string workingDirectoryPath;
public string aabFilePath;
public bool runOnDevice;
public override void RunPostBuildTask()
{
var buildToolLogger = new BuildToolLogger();
var appBundleBuilder = CreateAppBundleBuilder(workingDirectoryPath);
if (!appBundleBuilder.Initialize(buildToolLogger))
{
buildToolLogger.DisplayErrorDialog("Failed to initialize AppBundleBuilder in post-build task.");
return;
}
var assetPackConfig = SerializationHelper.Deserialize(serializableAssetPackConfig);
CreateBundleAsync(appBundleBuilder, aabFilePath, assetPackConfig, runOnDevice);
}
}
#endif
private static IBuildModeProvider _buildModeProvider;
#if UNITY_2018_4_OR_NEWER && !NET_LEGACY
/// <summary>
/// Builds an Android App Bundle given the specified configuration.
/// </summary>
public static async Task<AndroidBuildReport> BuildTask(AndroidBuildOptions androidBuildOptions)
{
var appBundleBuilder = CreateAppBundleBuilder();
if (!appBundleBuilder.Initialize(new BuildToolLogger()))
{
throw new Exception("Failed to initialize AppBundleBuilder");
}
return await appBundleBuilder.CreateBundleWithTask(androidBuildOptions);
}
/// <summary>
/// Builds an Android App Bundle containing only asset packs.
/// </summary>
public static async Task BuildAssetOnlyBundle(AssetOnlyBuildOptions assetOnlyBuildOptions)
{
var appBundleBuilder = CreateAppBundleBuilder();
if (!appBundleBuilder.Initialize(new BuildToolLogger()))
{
throw new Exception("Failed to initialize AppBundleBuilder");
}
await appBundleBuilder.CreateAssetOnlyBundle(assetOnlyBuildOptions);
}
#endif
/// <summary>
/// Builds an Android App Bundle given the specified configuration.
/// </summary>
public static bool Build(
BuildPlayerOptions buildPlayerOptions, AssetPackConfig assetPackConfig, bool forceSynchronousBuild)
{
var appBundleBuilder = CreateAppBundleBuilder();
if (!appBundleBuilder.Initialize(new BuildToolLogger()))
{
return false;
}
var buildSettings = new AppBundleBuildSettings
{
buildPlayerOptions = buildPlayerOptions,
assetPackConfig = assetPackConfig ?? new AssetPackConfig(),
forceSynchronousBuild = forceSynchronousBuild
};
return Build(appBundleBuilder, buildSettings);
}
/// <summary>
/// Builds an Android App Bundle at a location specified in a file save dialog.
/// The AAB will include asset packs configured in Asset Delivery Settings.
/// </summary>
public static void Build()
{
// Check that we're ready to build before displaying the file save dialog.
var appBundleBuilder = CreateAppBundleBuilder();
if (!appBundleBuilder.Initialize(new BuildToolLogger()))
{
return;
}
var aabFilePath = EditorUtility.SaveFilePanel("Create Android App Bundle", null, null, "aab");
if (string.IsNullOrEmpty(aabFilePath))
{
// Assume cancelled.
return;
}
var buildSettings = new AppBundleBuildSettings
{
buildPlayerOptions = AndroidBuildHelper.CreateBuildPlayerOptions(aabFilePath),
assetPackConfig = AssetPackConfigSerializer.LoadConfig()
};
Build(appBundleBuilder, buildSettings);
}
/// <summary>
/// Builds an Android App Bundle to a temp directory and then runs it on device.
/// </summary>
/// <param name="assetPackConfig">The asset pack configuration to use when building.</param>
public static void BuildAndRun(AssetPackConfig assetPackConfig)
{
var appBundleBuilder = CreateAppBundleBuilder();
if (!appBundleBuilder.Initialize(new BuildToolLogger()))
{
return;
}
var tempOutputFilePath = Path.Combine(appBundleBuilder.WorkingDirectoryPath, "temp.aab");
var buildSettings = new AppBundleBuildSettings
{
buildPlayerOptions = AndroidBuildHelper.CreateBuildPlayerOptions(tempOutputFilePath),
assetPackConfig = assetPackConfig,
runOnDevice = true
};
Build(appBundleBuilder, buildSettings);
}
/// <summary>
/// Sets the current BuildModeProvider. Should be called in a class annotated with "InitializeOnLoad" in order
/// for it to be assigned after assembly reload.
/// </summary>
public static void SetBuildModeProvider(IBuildModeProvider buildModeProvider)
{
_buildModeProvider = buildModeProvider;
}
private static bool Build(AppBundleBuilder appBundleBuilder, AppBundleBuildSettings buildSettings)
{
var androidBuildResult = appBundleBuilder.BuildAndroidPlayer(buildSettings.buildPlayerOptions);
if (!androidBuildResult.Succeeded)
{
return false;
}
var aabFilePath = buildSettings.buildPlayerOptions.locationPathName;
if (IsBatchMode || buildSettings.forceSynchronousBuild)
{
var createBundleOptions = new CreateBundleOptions
{
AabFilePath = aabFilePath, AssetPackConfig = buildSettings.assetPackConfig
};
var errorMessage = appBundleBuilder.CreateBundle(createBundleOptions);
return errorMessage == null;
}
#if UNITY_2018_3_OR_NEWER
CreateBundleAsync(appBundleBuilder, aabFilePath, buildSettings.assetPackConfig, buildSettings.runOnDevice);
#else
var task = new AppBundlePostBuildTask
{
serializableAssetPackConfig = SerializationHelper.Serialize(buildSettings.assetPackConfig),
workingDirectoryPath = appBundleBuilder.WorkingDirectoryPath,
aabFilePath = aabFilePath,
runOnDevice = buildSettings.runOnDevice
};
PostBuildRunner.RunTask(task);
#endif
// This return value should be ignored since the build is being finished asynchronously.
return true;
}
private static void CreateBundleAsync(
AppBundleBuilder appBundleBuilder, string aabFilePath, AssetPackConfig assetPackConfig, bool runOnDevice)
{
var callback =
runOnDevice
? (AppBundleBuilder.PostBuildCallback) RunBundle
: EditorUtility.RevealInFinder;
var createBundleOptions = new CreateBundleOptions
{
AabFilePath = aabFilePath, AssetPackConfig = assetPackConfig
};
appBundleBuilder.CreateBundleAsync(createBundleOptions, callback);
}
private static void RunBundle(string aabFile)
{
var buildToolLogger = new BuildToolLogger();
var appBundleRunner = CreateAppBundleRunner();
if (!appBundleRunner.Initialize(buildToolLogger))
{
buildToolLogger.DisplayErrorDialog("Failed to initialize AppBundleRunner.");
return;
}
appBundleRunner.RunBundle(aabFile, GetBuildMode());
}
private static AppBundleBuilder CreateAppBundleBuilder()
{
return CreateAppBundleBuilder(GetTempFolder());
}
private static AppBundleBuilder CreateAppBundleBuilder(string workingDirectoryPath)
{
var androidSdk = new AndroidSdk();
var javaUtils = new JavaUtils();
var androidSdkPlatform = new AndroidSdkPlatform(androidSdk);
var androidBuildTools = new AndroidBuildTools(androidSdk);
var jarSigner = new JarSigner(javaUtils);
return new AppBundleBuilder(
new AndroidAssetPackagingTool(androidBuildTools, androidSdkPlatform),
new AndroidBuilder(androidSdkPlatform),
new BundletoolHelper(javaUtils),
jarSigner,
workingDirectoryPath,
new ZipUtils(javaUtils));
}
private static AppBundleRunner CreateAppBundleRunner()
{
var androidSdk = new AndroidSdk();
var javaUtils = new JavaUtils();
var adb = new AndroidDebugBridge(androidSdk);
var bundletool = new BundletoolHelper(javaUtils);
return new AppBundleRunner(adb, bundletool);
}
private static string GetTempFolder()
{
// Create a new temp folder with each build. Some developers prefer a random path here since there may be
// multiple builds running concurrently, e.g. on an automated build machine. See Issue #69.
// Note: this plugin doesn't clear out old temporary build folders, so disk usage will grow over time.
// Note: we use the 2 argument Path.Combine() to support .NET 3.
return Path.Combine(Path.Combine(Path.GetTempPath(), "play-unity-build"), Path.GetRandomFileName());
}
private static BundletoolBuildMode GetBuildMode()
{
if (_buildModeProvider != null)
{
return _buildModeProvider.GetBuildMode();
}
return BundletoolBuildMode.Persistent;
}
private static bool IsBatchMode
{
get
{
#if UNITY_2018_2_OR_NEWER
return Application.isBatchMode;
#else
return Environment.CommandLine.Contains("-batchmode");
#endif
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 067a2af83af994d2bab4a749cba47b73
timeCreated: 1540331104
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
// Copyright 2021 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;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Build Options for building asset-only app bundles.
/// </summary>
public class AssetOnlyOptions
{
/// <summary>
/// A list of app versions that are eligible to update to this asset-only app bundle.
/// </summary>
public IList<long> AppVersions { get; set; }
/// <summary>
/// A string uniquely identifying this asset-only app bundle.
/// </summary>
public string AssetVersionTag { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36ce805c46a1445b5b499fbe670efae4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
// 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 Google.Android.AppBundle.Editor.Internal.AndroidManifest;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Provides a <see cref="Registry"/> of <see cref="IAssetPackManifestTransformer"/>s.
/// </summary>
public class AssetPackManifestTransformerRegistry : Registry<IAssetPackManifestTransformer>
{
private static AssetPackManifestTransformerRegistry _instance;
public static AssetPackManifestTransformerRegistry Registry
{
get
{
if (_instance == null)
{
_instance = new AssetPackManifestTransformerRegistry();
}
return _instance;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d02bb9eaf0454eeda560629d83c032ce
timeCreated: 1574886319

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f68087b1bfbaa4196b496b765b6f3b91
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
// 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;
using System.IO;
using System.Linq;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Internal-only representation of a folder containing AssetBundles.
/// </summary>
public class AssetBundleFolder
{
/// <summary>
/// The error state of this folder or <see cref="AssetPackFolderState.Ok"/>.
/// </summary>
public AssetPackFolderState State { get; private set; }
/// <summary>
/// The path represented by the folder.
/// </summary>
public string FolderPath { get; private set; }
/// <summary>
/// The texture compression format of AssetBundles inside this folder, based on the folder name.
/// </summary>
public TextureCompressionFormat TextureCompressionFormat { get; private set; }
/// <summary>
/// The number of AssetBundles found in this folder.
/// Note: this value is cached during calls to <see cref="ExtractAssetBundleVariant"/>.
/// </summary>
public int AssetBundleCount { get; private set; }
private string _searchedManifestFileName;
/// <summary>
/// Create a new AssetBundleFolder for a given path (which is then immutable).
/// Call <see cref="Refresh"/> to update the folder state and texture compression format targeting.
/// </summary>
public AssetBundleFolder(string folderPath)
{
AssetBundleCount = 0;
FolderPath = folderPath;
}
/// <summary>
/// Update the folder state and texture compression format targeting, if any.
/// </summary>
/// <returns>true if the refresh was properly done (no errors)</returns>
public bool Refresh()
{
var directoryInfo = new DirectoryInfo(FolderPath);
if (!directoryInfo.Exists)
{
AssetBundleCount = 0;
State = AssetPackFolderState.FolderMissing;
return false;
}
var files = directoryInfo.GetFiles();
if (files.Length == 0)
{
AssetBundleCount = 0;
State = AssetPackFolderState.FolderEmpty;
return false;
}
TextureCompressionFormat textureCompressionFormat;
TextureTargetingTools.GetTextureCompressionFormatAndStripSuffix(
directoryInfo.Name,
out textureCompressionFormat,
out _searchedManifestFileName);
TextureCompressionFormat = textureCompressionFormat;
return true;
}
/// <summary>
/// Scans and returns every <see cref="AssetBundleVariant"/> found in this folder.
/// </summary>
public IDictionary<string, AssetBundleVariant> ExtractAssetBundleVariant()
{
AssetBundleCount = 0;
var assetBundleVariants = new Dictionary<string, AssetBundleVariant>();
if (!Refresh())
{
return assetBundleVariants;
}
var directoryInfo = new DirectoryInfo(FolderPath);
var files = directoryInfo.GetFiles();
var fileDictionary = files.ToDictionary(file => file.Name, file => file);
// Look for an AssetBundle file with the same name as the directory that contains it.
// This AssetBundle file contains a single asset, which is of type AssetBundleManifest.
// See https://unity3d.com/learn/tutorials/topics/best-practices/assetbundle-fundamentals
FileInfo manifestFileInfo;
if (!fileDictionary.TryGetValue(_searchedManifestFileName, out manifestFileInfo))
{
State = AssetPackFolderState.ManifestFileMissing;
return assetBundleVariants;
}
var manifestFilePath = manifestFileInfo.FullName;
AssetBundle manifestAssetBundle;
try
{
manifestAssetBundle = AssetBundle.LoadFromFile(manifestFilePath);
}
catch (Exception ex)
{
Debug.LogErrorFormat("Exception loading AssetBundle file containing manifest ({0}): {1}",
manifestFilePath, ex);
State = AssetPackFolderState.ManifestFileLoadError;
return assetBundleVariants;
}
try
{
var manifest = manifestAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
if (manifest == null)
{
State = AssetPackFolderState.ManifestAssetMissing;
return assetBundleVariants;
}
var allAssetBundles = manifest.GetAllAssetBundles();
if (allAssetBundles.Length == 0)
{
State = AssetPackFolderState.AssetBundlesMissing;
return assetBundleVariants;
}
foreach (var assetBundleName in allAssetBundles)
{
FileInfo assetBundleFileInfo;
var fileSizeBytes = fileDictionary.TryGetValue(assetBundleName, out assetBundleFileInfo)
? assetBundleFileInfo.Length
: AssetBundleVariant.FileSizeIfMissing;
assetBundleVariants.Add(
assetBundleName,
AssetBundleVariant.CreateVariant(
assetBundleName,
fileSizeBytes,
manifest.GetDirectDependencies(assetBundleName),
manifest.GetAllDependencies(assetBundleName),
Path.Combine(FolderPath, assetBundleName)));
}
AssetBundleCount = assetBundleVariants.Count;
return assetBundleVariants;
}
catch (Exception ex)
{
Debug.LogErrorFormat(
"Exception loading AssetBundleManifest from AssetBundle ({0}): {1}", manifestFilePath, ex);
State = AssetPackFolderState.ManifestAssetLoadError;
return assetBundleVariants;
}
finally
{
// If an AssetBundle isn't unloaded, the Editor will have to be restarted to load it again.
manifestAssetBundle.Unload(true);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2c3bd268718fc41b48090fadb8eee0bd
timeCreated: 1548906103
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,170 @@
// 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.Collections.Generic;
using System.Linq;
using Google.Android.AppBundle.Editor.Internal.Utils;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Internal-only wrapper around <see cref="AssetBundleVariant"/>s that are identical except for their
/// texture compression formats.
/// </summary>
public class AssetBundlePack
{
/// <summary>
/// The name of the asset pack, which is the same as the name of every included AssetBundle.
/// </summary>
public readonly string Name;
/// <summary>
/// Indicates whether this pack will be included in an Android App Bundle, and if so how it is delivered.
/// </summary>
public AssetPackDeliveryMode DeliveryMode;
/// <summary>
/// The AssetBundles, all with the same name but generated for different texture compression formats.
/// </summary>
public IDictionary<TextureCompressionFormat, AssetBundleVariant> Variants { get; private set; }
/// <summary>
/// If non-empty, these errors will prevent this AssetBundlePack from being packaged in an App Bundle.
/// </summary>
public readonly HashSet<AssetPackError> Errors = new HashSet<AssetPackError>();
/// <summary>
/// Constructor.
/// </summary>
public AssetBundlePack(string name, AssetPackDeliveryMode deliveryMode)
{
Name = name;
DeliveryMode = deliveryMode;
Variants = new Dictionary<TextureCompressionFormat, AssetBundleVariant>();
}
/// <summary>
/// Adds an <see cref="AssetBundleVariant"/> to this asset pack.
/// <returns>true if the AssetBundle was added, false if another AssetBundle with the same compression
/// is already present.</returns>
/// </summary>
public bool Add(TextureCompressionFormat textureCompressionFormat, AssetBundleVariant variant)
{
AssetBundleVariant existingVariant;
if (Variants.TryGetValue(textureCompressionFormat, out existingVariant))
{
Debug.LogErrorFormat("Multiple AssetBundles for texture format {0} are used for {1}.",
textureCompressionFormat.ToString(), Name);
existingVariant.Errors.Add(AssetPackError.DuplicateName);
return false;
}
Variants.Add(textureCompressionFormat, variant);
return true;
}
/// <summary>
/// Adds the specified error to all children.
/// </summary>
public void AddErrorToVariants(AssetPackError error)
{
foreach (var assetPack in Variants.Values)
{
assetPack.Errors.Add(error);
}
}
/// <summary>
/// The names of any AssetBundles that the AssetBundles inside this AssetBundlePack directly depend on.
/// </summary>
public IList<string> DirectDependencies
{
get
{
var directDependencies =
Variants.Values.SelectMany(pack => pack.DirectDependencies).Distinct().ToList();
directDependencies.Sort();
return directDependencies;
}
}
/// <summary>
/// A UI friendly way of displaying the error with this AssetBundlePack (if one) or the number of errors
/// (if more than one). If there are no errors, returns null.
/// </summary>
public string ErrorSummary
{
get
{
switch (Errors.Count)
{
case 0:
return null;
case 1:
return NameAndDescriptionAttribute.GetAttribute(Errors.First()).Name;
default:
return Errors.Count + " Errors";
}
}
}
/// <summary>
/// Delegate to find an AssetBundlePack when doing the dependencies check.
/// </summary>
/// <param name="name">The name of the other AssetBundlePack.</param>
/// <param name="assetBundlePack">If found, will contain the AssetBundlePack.</param>
public delegate bool TryGetAssetBundlePack(string name, out AssetBundlePack assetBundlePack);
/// <summary>
/// Evaluates whether to set any <see cref="Errors"/> based on problems with the dependencies.
/// </summary>
/// <param name="tryGetAssetBundlePack">Function to get an <see cref="AssetBundlePack"/> given its name.</param>
public void CheckDependencyErrors(TryGetAssetBundlePack tryGetAssetBundlePack)
{
if (DeliveryMode == AssetPackDeliveryMode.DoNotPackage)
{
return;
}
foreach (var variant in Variants)
{
var textureCompressionFormat = variant.Key;
var assetPack = variant.Value;
assetPack.CheckDependencyErrors(
DeliveryMode,
TryGetDependency(tryGetAssetBundlePack, textureCompressionFormat));
}
}
private static AssetBundleVariant.TryGetDependency TryGetDependency(
TryGetAssetBundlePack tryGetAssetBundlePack, TextureCompressionFormat textureCompressionFormat)
{
return (string name, out AssetBundleVariant dependency, out AssetPackDeliveryMode deliveryMode) =>
{
deliveryMode = AssetPackDeliveryMode.DoNotPackage;
dependency = null;
AssetBundlePack assetBundlePack;
if (tryGetAssetBundlePack(name, out assetBundlePack) &&
assetBundlePack.Variants.TryGetValue(textureCompressionFormat, out dependency))
{
deliveryMode = assetBundlePack.DeliveryMode;
return true;
}
return false;
};
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20e43cae4a4245538a68b920730857b6
timeCreated: 1571324952

View File

@@ -0,0 +1,206 @@
// 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.Collections.Generic;
using System.Linq;
using Google.Android.AppBundle.Editor.Internal.Utils;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Internal-only wrapper around an AssetBundle that by virtue of its texture compression format is a
/// variant of the other AssetBundles contained in an <see cref="AssetBundlePack"/>.
/// </summary>
public class AssetBundleVariant
{
/// <summary>
/// Special value for <see cref="FileSizeBytes"/> indicating the AssetBundle file does not exist.
/// </summary>
public const long FileSizeIfMissing = -1L;
// Visible for testing.
public const string FileMissingText = "file missing";
// Visible for testing.
public const string NoneText = "none";
private static readonly string[] EmptyStringArray = new string[0];
/// <summary>
/// Constructor. Prefer <see cref="CreateVariant"/> for all cases except deserialization and testing.
/// </summary>
public AssetBundleVariant()
{
DirectDependencies = EmptyStringArray;
AllDependencies = EmptyStringArray;
}
/// <summary>
/// If non-empty, these errors will prevent this AssetBundle from being packaged in an app bundle.
/// </summary>
public readonly HashSet<AssetPackError> Errors = new HashSet<AssetPackError>();
/// <summary>
/// The names of any AssetBundles that this one directly depends on.
/// </summary>
public string[] DirectDependencies { get; private set; }
/// <summary>
/// The names of any AssetBundles that this one directly or transitively depends on.
/// </summary>
public string[] AllDependencies { get; private set; }
/// <summary>
/// The size of the file in bytes, or -1L if the file doesn't exist.
/// </summary>
public long FileSizeBytes { get; private set; }
/// <summary>
/// The path to the AssetBundle represented by this wrapper.
/// </summary>
public string Path { get; private set; }
/// <summary>
/// A UI friendly way of displaying dependency info.
/// </summary>
public string DependenciesText
{
// TODO: consider how to handle an extra long string in the UI.
get { return DirectDependencies.Length == 0 ? NoneText : string.Join(", ", DirectDependencies); }
}
/// <summary>
/// A UI friendly way of displaying file size or that the file doesn't exist on disk.
/// </summary>
public string FileSizeText
{
get
{
// Display file size in Kibibytes using "JEDEC" standard. See https://en.wikipedia.org/wiki/Kilobyte
return FileSizeBytes == FileSizeIfMissing ? FileMissingText : (FileSizeBytes / 1024) + " KB";
}
}
/// <summary>
/// A UI friendly way of displaying the error with this AssetBundle (if there is one) or the number of errors
/// (if there is more than one). If there are no errors, this is null.
/// </summary>
public string ErrorSummary
{
get
{
switch (Errors.Count)
{
case 0:
return null;
case 1:
return NameAndDescriptionAttribute.GetAttribute(Errors.First()).Name;
default:
return Errors.Count + " Errors";
}
}
}
/// <summary>
/// Delegate to find a variant when doing the dependencies check.
/// See <see cref="AssetBundleVariant.CheckDependencyErrors"/>.
/// </summary>
/// <param name="name">The asset pack to search.</param>
/// <param name="dependencyVariant">If found, will contain the searched asset pack.</param>
/// <param name="dependencyDeliveryMode">If found, will contain the delivery mode of the searched asset pack.</param>
/// <returns>true if the asset pack was found, false otherwise.</returns>
public delegate bool TryGetDependency(string name, out AssetBundleVariant dependencyVariant,
out AssetPackDeliveryMode dependencyDeliveryMode);
/// <summary>
/// Evaluates whether to set any <see cref="Errors"/> based on problems with this variant's dependencies.
/// </summary>
/// <param name="deliveryMode">The delivery mode of this asset pack.</param>
/// <param name="tryGetDependency">A function returning the asset pack for the given dependency name,
/// along with the delivery mode of the asset pack.</param>
public void CheckDependencyErrors(AssetPackDeliveryMode deliveryMode, TryGetDependency tryGetDependency)
{
if (deliveryMode == AssetPackDeliveryMode.DoNotPackage)
{
return;
}
foreach (var dependencyName in AllDependencies)
{
AssetBundleVariant dependencyVariant;
AssetPackDeliveryMode dependencyDeliveryMode;
if (!tryGetDependency(dependencyName, out dependencyVariant, out dependencyDeliveryMode))
{
Errors.Add(AssetPackError.DependencyMissing);
continue;
}
if (dependencyVariant.Errors.Count > 0)
{
Errors.Add(AssetPackError.DependencyError);
}
// If this asset pack is marked for delivery, all of its dependencies must be packaged for delivery.
if (dependencyDeliveryMode == AssetPackDeliveryMode.DoNotPackage)
{
Errors.Add(AssetPackError.DependencyNotPackaged);
continue;
}
// An asset pack cannot have dependencies on asset packs with later delivery modes,
// e.g. A FastFollow asset pack cannot depend on an OnDemand asset pack.
if (deliveryMode < dependencyDeliveryMode)
{
Errors.Add(AssetPackError.DependencyIncompatibleDelivery);
}
}
}
/// <summary>
/// Creates a new <see cref="AssetBundleVariant"/>. Evaluates some conditions and may set <see cref="Errors"/>.
/// </summary>
/// <param name="assetBundleName">Name of the AssetBundle represented by this variant.</param>
/// <param name="fileSizeBytes">Size of the file in bytes, or <see cref="FileSizeIfMissing"/>.</param>
/// <param name="directDependencies">AssetBundles that are direct dependencies of this one.</param>
/// <param name="allDependencies">AssetBundles that are direct or transitive dependencies of this one.</param>
/// <param name="path">The path to the AssetBundle represented by this variant.</param>
public static AssetBundleVariant CreateVariant(
string assetBundleName,
long fileSizeBytes,
string[] directDependencies,
string[] allDependencies,
string path)
{
var variant = new AssetBundleVariant
{
DirectDependencies = directDependencies ?? EmptyStringArray,
AllDependencies = allDependencies ?? EmptyStringArray,
FileSizeBytes = fileSizeBytes,
Path = path,
};
if (fileSizeBytes == FileSizeIfMissing)
{
variant.Errors.Add(AssetPackError.FileMissing);
}
if (!AndroidAppBundle.IsValidModuleName(assetBundleName))
{
variant.Errors.Add(AssetPackError.InvalidName);
}
return variant;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6c348752fa8c94d3a8f6f085846aa228
timeCreated: 1548906103
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,251 @@
// 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.Collections.Generic;
using System.Linq;
using Google.Android.AppBundle.Editor.Internal.Config;
using UnityEditor;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Internal-only representation of asset packs being considered for packaging in an Android App Bundle.
/// </summary>
public class AssetDeliveryConfig
{
/// <summary>
/// Default texture compression format for building standalone APKs for Android pre-Lollipop devices.
/// </summary>
public TextureCompressionFormat DefaultTextureCompressionFormat = TextureCompressionFormat.Default;
/// <summary>
/// Whether to split the assets in an AAB's base module into a separate install-time asset pack.
/// </summary>
public bool SplitBaseModuleAssets;
/// <summary>
/// Dictionary from a folder path to a AssetBundleFolder object describing the folder's contents.
/// </summary>
public readonly IDictionary<string, AssetBundleFolder> Folders =
new SortedDictionary<string, AssetBundleFolder>();
/// <summary>
/// Dictionary from AssetBundle name to AssetBundlePacks.
/// </summary>
public IDictionary<string, AssetBundlePack> AssetBundlePacks { get; private set; }
// TODO(b/150701341): add support for managing this type of asset pack in the UI.
public List<SerializableAssetPack> rawAssetsPacks;
private readonly IEnumerable<IAssetPackValidator> _packValidators;
public AssetDeliveryConfig()
{
AssetBundlePacks = new Dictionary<string, AssetBundlePack>();
_packValidators = AssetPackValidatorRegistry.Registry.ConstructInstances();
}
/// <summary>
/// Updates the in-memory state based on the AssetBundles on disk.
/// </summary>
public void Refresh()
{
// Refresh the AssetBundlePacks according to AssetBundles contained in folders.
RefreshAssetBundlePacks();
// Check for errors.
foreach (var assetBundlePack in AssetBundlePacks.Values)
{
CheckBuildTypeErrors(assetBundlePack);
assetBundlePack.CheckDependencyErrors(
(string name, out AssetBundlePack pack) => AssetBundlePacks.TryGetValue(name, out pack));
}
}
/// <summary>
/// Potentially refresh all folders and/or add and remove the specified folders.
/// </summary>
/// <returns>true if changes were made.</returns>
public bool UpdateAndRefreshFolders(bool refreshFolders, List<string> foldersToAdd,
List<string> foldersToRemove)
{
if (!refreshFolders && foldersToRemove.Count == 0 && foldersToAdd.Count == 0)
{
return false;
}
foreach (var folder in foldersToAdd)
{
if (Folders.ContainsKey(folder))
{
Debug.LogWarningFormat("Skipping folder \"{0}\" because it is already in the list.", folder);
}
else
{
Folders.Add(folder, new AssetBundleFolder(folder));
}
}
foreach (var folder in foldersToRemove)
{
Folders.Remove(folder);
}
Refresh();
return true;
}
/// <summary>
/// Return a list of error messages that are applicable to AssetBundles marked for packaging/delivery
/// along with the number of AssetBundles that are marked for packaging/delivery.
/// </summary>
public List<string> GetPackagingErrorMessages(out int numAssetPacksToDeliver)
{
// Check for errors in asset packs
numAssetPacksToDeliver = 0;
var errors = new List<string>();
foreach (var assetBundlePack in AssetBundlePacks.Values)
{
if (assetBundlePack.DeliveryMode == AssetPackDeliveryMode.DoNotPackage)
{
continue;
}
var errorSummary = assetBundlePack.ErrorSummary;
if (errorSummary != null)
{
errors.Add(string.Format(
"Unable to package AssetBundle pack \"{0}\" because {1}.",
assetBundlePack.Name, errorSummary.ToLower()));
}
numAssetPacksToDeliver++;
foreach (var variant in assetBundlePack.Variants.Values)
{
var variantErrorSummary = variant.ErrorSummary;
if (variantErrorSummary != null)
{
errors.Add(string.Format(
"Unable to package AssetBundle \"{0}\" because {1}.",
variant.Path, variantErrorSummary.ToLower()));
}
}
}
// Check for errors in the rest of the configuration.
if (HasTextureCompressionFormatTargeting() &&
!GetAllTextureCompressionFormats().Contains(DefaultTextureCompressionFormat))
{
errors.Add(string.Format(
"The default texture compression format ({0}) is not used by any AssetBundles.",
DefaultTextureCompressionFormat.ToString()));
}
// TODO: think about moving to a build check.
if (HasTextureCompressionFormatTargeting() &&
EditorUserBuildSettings.androidBuildSubtarget != MobileTextureSubtarget.Generic)
{
errors.Add(string.Format(
"Texture Compression in the Build Settings for Android is set to \"{0}\". This is not compatible with asset packs having targeted textures. Set the texture compression in the Build Settings for Android to \"Don't override'\" before continuing.",
EditorUserBuildSettings.androidBuildSubtarget));
}
return errors;
}
/// <summary>
/// Refresh the list of all the <see cref="AssetBundlePacks"/> represented by the folders.
/// </summary>
public void RefreshAssetBundlePacks()
{
var assetBundlePacks = new Dictionary<string, AssetBundlePack>();
foreach (var folder in Folders.Values)
{
var assetPacks = folder.ExtractAssetBundleVariant();
foreach (var targetedAssetPackPair in assetPacks)
{
var targetedAssetPackName = targetedAssetPackPair.Key;
var targetedAssetPack = targetedAssetPackPair.Value;
AssetBundlePack assetBundlePack;
if (!assetBundlePacks.TryGetValue(targetedAssetPackName, out assetBundlePack))
{
var deliveryMode = GetMultiTargetingAssetPackDeliveryMode(targetedAssetPackName);
assetBundlePack = new AssetBundlePack(targetedAssetPackName, deliveryMode);
assetBundlePacks.Add(targetedAssetPackName, assetBundlePack);
}
if (!assetBundlePack.Add(folder.TextureCompressionFormat, targetedAssetPack))
{
assetBundlePack.Errors.Add(AssetPackError.DuplicateName);
}
}
}
AssetBundlePacks = assetBundlePacks;
}
/// <summary>
/// Returns the delivery mode for the AssetBundlePack with the given name, or DoNotPackage if not found.
/// </summary>
private AssetPackDeliveryMode GetMultiTargetingAssetPackDeliveryMode(string multiTargetingAssetPackName)
{
AssetBundlePack assetBundlePack;
if (AssetBundlePacks.TryGetValue(multiTargetingAssetPackName, out assetBundlePack))
{
return assetBundlePack.DeliveryMode;
}
return AssetPackDeliveryMode.DoNotPackage;
}
/// <summary>
/// Returns all texture compression formats used by asset packs.
/// </summary>
public IEnumerable<TextureCompressionFormat> GetAllTextureCompressionFormats()
{
return AssetBundlePacks.Values.SelectMany(
multiTargetingAssetPack => multiTargetingAssetPack.Variants.Keys).Distinct();
}
/// <summary>
/// Check if some asset packs are using targeted texture compression format.
/// </summary>
public bool HasTextureCompressionFormatTargeting()
{
return GetAllTextureCompressionFormats().Any(textureCompressionFormat =>
textureCompressionFormat != TextureCompressionFormat.Default);
}
/// <summary>
/// Checks the specified bundlePack for build errors using validators registered in AssetPackValidatorRegistry.
/// </summary>
private void CheckBuildTypeErrors(AssetBundlePack bundlePack)
{
var buildErrors = new HashSet<AssetPackError>();
foreach (var buildValidator in _packValidators)
{
buildErrors.UnionWith(buildValidator.CheckBuildErrors(bundlePack));
}
foreach (var error in buildErrors)
{
bundlePack.AddErrorToVariants(error);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: bdc061489f3c64fd78ed0cefe895b007
timeCreated: 1549914493
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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.Collections.Generic;
using System.IO;
using System.Linq;
using Google.Android.AppBundle.Editor.Internal.Config;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Provides methods for serializing <see cref="AssetDeliveryConfig"/> to a JSON file and for deserializing it.
/// </summary>
public static class AssetDeliveryConfigSerializer
{
/// <summary>
/// Save the specified AssetDeliveryConfig config to disk.
/// </summary>
public static void SaveConfig(AssetDeliveryConfig assetDeliveryConfig)
{
Debug.LogFormat("Saving {0}", SerializationHelper.ConfigurationFilePath);
var config = Serialize(assetDeliveryConfig);
var jsonText = JsonUtility.ToJson(config);
File.WriteAllText(SerializationHelper.ConfigurationFilePath, jsonText);
}
/// <summary>
/// Returns an AssetDeliveryConfig loaded from disk.
/// </summary>
public static AssetDeliveryConfig LoadConfig()
{
Debug.LogFormat("Loading {0}", SerializationHelper.ConfigurationFilePath);
SerializableAssetPackConfig config;
if (File.Exists(SerializationHelper.ConfigurationFilePath))
{
var jsonText = File.ReadAllText(SerializationHelper.ConfigurationFilePath);
config = JsonUtility.FromJson<SerializableAssetPackConfig>(jsonText);
}
else
{
config = new SerializableAssetPackConfig();
}
return Deserialize(config);
}
private static SerializableAssetPackConfig Serialize(AssetDeliveryConfig assetDeliveryConfig)
{
var config = new SerializableAssetPackConfig
{
DefaultTextureCompressionFormat = assetDeliveryConfig.DefaultTextureCompressionFormat,
splitBaseModuleAssets = assetDeliveryConfig.SplitBaseModuleAssets
};
foreach (var assetPack in assetDeliveryConfig.AssetBundlePacks.Values)
{
config.assetBundles.Add(new SerializableMultiTargetingAssetBundle
{
name = assetPack.Name,
DeliveryMode = assetPack.DeliveryMode,
assetBundles = assetPack.Variants.Select(pack => new SerializableAssetBundle
{
path = pack.Value.Path,
TextureCompressionFormat = pack.Key
}).ToList()
});
}
config.assetPacks = assetDeliveryConfig.rawAssetsPacks;
return config;
}
private static AssetDeliveryConfig Deserialize(SerializableAssetPackConfig config)
{
var assetDeliveryConfig = new AssetDeliveryConfig
{
DefaultTextureCompressionFormat = config.DefaultTextureCompressionFormat,
SplitBaseModuleAssets = config.splitBaseModuleAssets
};
var paths = new HashSet<string>();
foreach (var multiTargetingAssetBundle in config.assetBundles)
{
paths.UnionWith(
multiTargetingAssetBundle.assetBundles.Select(item => Path.GetDirectoryName(item.path)));
assetDeliveryConfig.AssetBundlePacks.Add(multiTargetingAssetBundle.name,
new AssetBundlePack(multiTargetingAssetBundle.name,
multiTargetingAssetBundle.DeliveryMode));
}
paths.ToList().ForEach(path => assetDeliveryConfig.Folders.Add(path, new AssetBundleFolder(path)));
assetDeliveryConfig.rawAssetsPacks = config.assetPacks;
return assetDeliveryConfig;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f127ef3c767384ec89ad3e3aef7ae1bb
timeCreated: 1548906103
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,399 @@
// 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.Collections.Generic;
using System.Linq;
using Google.Android.AppBundle.Editor.Internal.Utils;
using UnityEditor;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
public class AssetDeliveryWindow : EditorWindow
{
/// <summary>
/// Label for the split base APK checkbox. Visible for referencing from error messages.
/// </summary>
public const string SeparateAssetsLabel = "Separate Base APK Assets";
private const string RefreshButtonText = "Refresh";
private const int WindowMinWidth = 610;
private const int WindowMinHeight = 300;
private const int ButtonWidth = 150;
private const int FieldWidth = 100;
private AssetDeliveryConfig _assetDeliveryConfig;
private Vector2 _scrollPosition;
/// <summary>
/// The names of asset packs that are shown collapsed.
/// </summary>
private HashSet<string> _collapsedAssetPacks;
/// <summary>
/// Displays this window, creating it if necessary.
/// </summary>
public static void ShowWindow()
{
var window = GetWindow(typeof(AssetDeliveryWindow), false, "Asset Delivery");
window.minSize = new Vector2(WindowMinWidth, WindowMinHeight);
}
private void OnGUI()
{
if (_assetDeliveryConfig == null)
{
_assetDeliveryConfig = AssetDeliveryConfigSerializer.LoadConfig();
_assetDeliveryConfig.Refresh();
}
if (_collapsedAssetPacks == null)
{
_collapsedAssetPacks = new HashSet<string>();
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Base APK Asset Delivery", EditorStyles.boldLabel);
RenderDescription(
"When building an Android App Bundle (AAB), automatically move assets that would normally be " +
"delivered in the base APK into an install-time asset pack. This option reduces base APK size " +
"similarly to the \"Split Application Binary\" publishing setting, but it uses Play Asset Delivery " +
"instead of APK Expansion (OBB) files, so it's compatible with AABs. This option is recommended for " +
"apps with a large base APK, e.g. over 150 MB.");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(SeparateAssetsLabel, GUILayout.Width(145));
var pendingSplitBaseModuleAssets = EditorGUILayout.Toggle(_assetDeliveryConfig.SplitBaseModuleAssets);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Asset Pack Configuration", EditorStyles.boldLabel);
RenderDescription(
"Add folders that directly contain AssetBundle files, for example the output folder from Unity's " +
"AssetBundle Browser. All AssetBundles directly contained in the specified folders will be displayed " +
"below. Update the \"Delivery Mode\" to include an AssetBundle in AAB builds.");
EditorGUILayout.Space();
// TODO(b/144677274): Remove this #if once TCF support is available in Play, to make TCF targeting discoverable.
#if SHOW_TCF_IN_UI
if (_assetDeliveryConfig.Folders.Count != 0 && !_assetDeliveryConfig.HasTextureCompressionFormatTargeting())
{
// TODO(b/144677274): Add link to public documentation when available.
EditorGUILayout.HelpBox(
"These AssetBundles don't specify texture compression format targeting. Generate additional " +
"AssetBundles into folders ending with #tcf_xxx (e.g. AssetBundles#tcf_astc), and the " +
"AssetBundles will be delivered to devices supporting that format.", MessageType.Info);
EditorGUILayout.Space();
}
#endif
EditorGUILayout.BeginHorizontal();
// Store the changes and defer the actual update at the end to avoid modifications during rendering.
var refreshAssetDeliveryConfig = false;
var foldersToRemove = new List<string>();
var foldersToAdd = new List<string>();
if (GUILayout.Button("Add Folder...", GUILayout.Width(ButtonWidth)))
{
var folderPath = EditorUtility.OpenFolderPanel("Add Folder Containing AssetBundles", null, null);
if (string.IsNullOrEmpty(folderPath))
{
// Assume cancelled.
return;
}
if (_assetDeliveryConfig.Folders.ContainsKey(folderPath))
{
var errorMessage =
string.Format(
"Cannot add a folder that has already been added. Use \"{0}\" to analyze existing folders.",
RefreshButtonText);
EditorUtility.DisplayDialog("Add Folder Error", errorMessage, WindowUtils.OkButtonText);
}
else
{
foldersToAdd.Add(folderPath);
}
}
if (GUILayout.Button(RefreshButtonText, GUILayout.Width(ButtonWidth)))
{
refreshAssetDeliveryConfig = true;
}
if (GUILayout.Button("App Bundle Summary...", GUILayout.Width(ButtonWidth)))
{
_assetDeliveryConfig.Refresh();
EditorUtility.DisplayDialog("App Bundle Summary", GetPackagingSummary(), WindowUtils.OkButtonText);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Hide this setting for projects targeting SDK 21+ since it only affects install-time asset pack
// installation on older devices.
if (_assetDeliveryConfig.HasTextureCompressionFormatTargeting() &&
TextureTargetingTools.IsSdkVersionPreLollipop(PlayerSettings.Android.minSdkVersion))
{
EditorGUILayout.LabelField("Texture Compression Configuration", EditorStyles.boldLabel);
EditorGUILayout.Space();
bool targetingUpdated;
RenderTextureCompressionFormatTargetingConfiguration(_assetDeliveryConfig, out targetingUpdated);
refreshAssetDeliveryConfig |= targetingUpdated;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUILayout.LabelField("AssetBundle Folders", EditorStyles.boldLabel);
EditorGUILayout.Space();
foreach (var folderPair in _assetDeliveryConfig.Folders)
{
var folderPath = folderPair.Key;
bool removeFolder;
RenderFolder(folderPath, folderPair.Value, out removeFolder);
if (removeFolder)
{
foldersToRemove.Add(folderPath);
}
}
if (_assetDeliveryConfig.Folders.Count == 0)
{
EditorGUILayout.HelpBox("Add folders that directly contain AssetBundles.", MessageType.None);
EditorGUILayout.Space();
}
else
{
EditorGUILayout.LabelField("AssetBundle Configuration", EditorStyles.boldLabel);
EditorGUILayout.Space();
foreach (var assetBundlePack in _assetDeliveryConfig.AssetBundlePacks.Values)
{
bool refreshFolder;
RenderAssetBundlePack(assetBundlePack, out refreshFolder);
refreshAssetDeliveryConfig |= refreshFolder;
}
}
EditorGUILayout.EndScrollView();
// Defer folder refresh, addition and removal to avoid modification of the collection while iterating.
var pendingConfigChanges =
_assetDeliveryConfig.UpdateAndRefreshFolders(refreshAssetDeliveryConfig, foldersToAdd, foldersToRemove);
pendingConfigChanges |= pendingSplitBaseModuleAssets != _assetDeliveryConfig.SplitBaseModuleAssets;
if (pendingConfigChanges)
{
_assetDeliveryConfig.SplitBaseModuleAssets = pendingSplitBaseModuleAssets;
AssetDeliveryConfigSerializer.SaveConfig(_assetDeliveryConfig);
}
}
private void RenderAssetBundlePack(AssetBundlePack assetBundlePack, out bool refreshFolder)
{
refreshFolder = false;
#if UNITY_2019_1_OR_NEWER
var showGroupExpanded = !_collapsedAssetPacks.Contains(assetBundlePack.Name);
var groupExpanded =
EditorGUILayout.BeginFoldoutHeaderGroup(showGroupExpanded, assetBundlePack.Name);
if (groupExpanded)
{
_collapsedAssetPacks.Remove(assetBundlePack.Name);
RenderAssetBundlePackContent(assetBundlePack, out refreshFolder);
}
else
{
_collapsedAssetPacks.Add(assetBundlePack.Name);
}
EditorGUILayout.EndFoldoutHeaderGroup();
#else
// Old Unity versions don't support foldable headers. Use a simple label instead.
EditorGUILayout.LabelField(assetBundlePack.Name, EditorStyles.boldLabel);
RenderAssetBundlePackContent(assetBundlePack, out refreshFolder);
EditorGUILayout.Separator();
#endif
}
private static void RenderAssetBundlePackContent(AssetBundlePack assetBundlePack, out bool refreshFolder)
{
refreshFolder = false;
EditorGUI.BeginChangeCheck();
var newDeliveryMode =
(AssetPackDeliveryMode) EditorGUILayout.EnumPopup("Delivery Mode", assetBundlePack.DeliveryMode,
GUILayout.Width(300));
if (EditorGUI.EndChangeCheck())
{
assetBundlePack.DeliveryMode = newDeliveryMode;
// Don't use a short-circuit evaluation one-liner to ensure EndChangeCheck() is called above.
refreshFolder = true;
}
// Display the direct dependencies of all the contained asset packs.
var directDependencies = assetBundlePack.DirectDependencies;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Dependencies");
GUILayout.TextArea(
directDependencies.Count > 0 ? string.Join(", ", directDependencies.ToArray()) : "None");
EditorGUILayout.EndHorizontal();
EditorGUILayout.LabelField("AssetBundles");
EditorGUI.indentLevel++;
foreach (var variant in assetBundlePack.Variants)
{
RenderVariant(variant.Value, variant.Key);
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
}
private static void RenderVariant(AssetBundleVariant variant, TextureCompressionFormat targeting)
{
EditorGUILayout.BeginHorizontal();
// TODO: revisit TextureCompressionFormat enum extension methods.
var targetingDescription = targeting == TextureCompressionFormat.Default
? targeting.ToString()
: targeting.ToString().ToUpper();
EditorGUILayout.PrefixLabel(targetingDescription);
EditorGUILayout.LabelField(variant.FileSizeText, GUILayout.ExpandWidth(false));
var errors = variant.Errors;
if (errors.Count == 0)
{
EditorGUILayout.LabelField("No errors", GUILayout.ExpandWidth(true));
}
else
{
if (GUILayout.Button(variant.ErrorSummary + "...", GUILayout.ExpandWidth(true)))
{
var errorDialogTitle = "AssetBundle Error" + (errors.Count == 1 ? string.Empty : "s");
var errorDialogMessage = string.Join("\n\n",
errors.Select(e => NameAndDescriptionAttribute.GetAttribute(e).Description).ToArray());
EditorUtility.DisplayDialog(errorDialogTitle, errorDialogMessage, WindowUtils.OkButtonText);
}
}
EditorGUILayout.EndHorizontal();
}
private static void RenderFolder(string folderPath, AssetBundleFolder assetBundleFolder, out bool removeFolder)
{
// Folder path and associated management buttons.
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(folderPath, EditorStyles.wordWrappedLabel);
removeFolder = GUILayout.Button("Remove", GUILayout.Width(FieldWidth));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Display the folder state.
if (assetBundleFolder.State == AssetPackFolderState.Ok)
{
EditorGUILayout.HelpBox(
string.Format("Found {0} AssetBundle(s) in this folder", assetBundleFolder.AssetBundleCount),
MessageType.None);
}
else
{
var errorMessage = NameAndDescriptionAttribute.GetAttribute(assetBundleFolder.State).Description;
EditorGUILayout.HelpBox(errorMessage, MessageType.Warning);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
}
private static void RenderTextureCompressionFormatTargetingConfiguration(
AssetDeliveryConfig assetDeliveryConfig, out bool targetingUpdated)
{
targetingUpdated = false;
const string label = "Format for pre-L devices";
var width = GUILayout.Width(300);
EditorGUI.BeginChangeCheck();
#if UNITY_2018_3_OR_NEWER
var validTextureCompressionFormats = assetDeliveryConfig.GetAllTextureCompressionFormats();
var newDefaultTextureCompressionFormat =
(TextureCompressionFormat) EditorGUILayout.EnumPopup(new GUIContent(label),
assetDeliveryConfig.DefaultTextureCompressionFormat,
textureCompressionFormat =>
validTextureCompressionFormats.Contains((TextureCompressionFormat) textureCompressionFormat),
false, width);
#else
var newDefaultTextureCompressionFormat =
(TextureCompressionFormat) EditorGUILayout.EnumPopup(label,
assetDeliveryConfig.DefaultTextureCompressionFormat, width);
#endif
if (EditorGUI.EndChangeCheck())
{
assetDeliveryConfig.DefaultTextureCompressionFormat = newDefaultTextureCompressionFormat;
targetingUpdated = true;
}
EditorGUILayout.HelpBox(
"This compression format will be used for devices running Android 4.4.4 (SDK 20) or older. " +
"AssetBundles marked as install-time will be included in the APK generated for these devices. " +
"Android 5.0 (SDK 21) and newer devices aren't affected by this setting.",
MessageType.None);
}
private static void RenderDescription(string label)
{
var descriptionTextStyle = new GUIStyle(GUI.skin.label)
{
fontStyle = FontStyle.Italic,
wordWrap = true
};
EditorGUILayout.BeginVertical("textfield"); // Adds a light grey background.
EditorGUILayout.Space();
EditorGUILayout.LabelField(label, descriptionTextStyle);
EditorGUILayout.Space();
EditorGUILayout.EndVertical();
}
private string GetPackagingSummary()
{
int numAssetPacksToDeliver;
var errorMessages = _assetDeliveryConfig.GetPackagingErrorMessages(out numAssetPacksToDeliver);
if (numAssetPacksToDeliver == 0)
{
return _assetDeliveryConfig.SplitBaseModuleAssets
? "The Base APK's assets will be packaged in an asset pack."
: "There are no AssetBundles marked for packaging.";
}
if (errorMessages.Count > 0)
{
const string separator = "\n\n- ";
return "The following error(s) will occur when building an Android App Bundle:" + separator +
string.Join(separator, errorMessages.ToArray());
}
var description = numAssetPacksToDeliver == 1
? "There is 1 AssetBundle marked for packaging."
: string.Format("There are {0} AssetBundles marked for packaging.", numAssetPacksToDeliver);
return _assetDeliveryConfig.SplitBaseModuleAssets
? description + " Also, the Base APK's assets will be packaged in an asset pack."
: description;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2fe34ebc6742d4f818529bc14c64c974
timeCreated: 1548906103
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Describes various possible error states of an AssetPack.
/// </summary>
public enum AssetPackError
{
[NameAndDescription("Duplicate Name",
"AssetBundles with the same name and texture format exist in different folders. An " +
"AssetBundle's name and parent folder texture format must be unique.")]
DuplicateName,
[NameAndDescription("Invalid Name",
"A Play-delivered AssetBundle's name must start with an English letter and can only contain letters, " +
"numbers, and underscores. Please regenerate the AssetBundle with a new name.")]
InvalidName,
[NameAndDescription("Missing File", "An AssetBundle file with this name is missing from the folder.")]
FileMissing,
[NameAndDescription("Dependency Error", "One or more of this AssetBundle's dependencies has an error.")]
DependencyError,
[NameAndDescription("Missing Dependency", "One or more of this AssetBundle's dependencies is missing.")]
DependencyMissing,
[NameAndDescription("Dependency Not Packaged",
"This AssetBundle is marked for delivery, but one or more of its dependencies is not.")]
DependencyNotPackaged,
[NameAndDescription("Incompatible Dependency",
"This AssetBundle is marked to be delivered earlier than one or more of its dependencies.")]
DependencyIncompatibleDelivery,
[NameAndDescription("Instant Incompatible",
"Install-time asset packs aren't supported for instant apps.")]
InstallTimeAndInstant,
[NameAndDescription("Instant Incompatible",
"Fast-follow asset packs aren't supported for instant apps.")]
FastFollowAndInstant,
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8cad3b640bc35422c88ba5dd808e4170
timeCreated: 1549483271
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Describes various possible states of a folder containing AssetBundle files.
/// </summary>
public enum AssetPackFolderState
{
// Does not need NameAndDescription attribute.
Ok,
[NameAndDescription("Folder Missing", "This folder does not exist.")]
FolderMissing,
[NameAndDescription("Folder Empty", "This folder contains no files.")]
FolderEmpty,
[NameAndDescription("Manifest File Missing",
"This folder is missing an AssetBundle file that matches the folder name.")]
ManifestFileMissing,
[NameAndDescription("Manifest File Load Error",
"There was an error loading the AssetBundle containing the AssetBundleManifest asset.")]
ManifestFileLoadError,
[NameAndDescription("AssetBundleManifest Missing",
"This folder's AssetBundle manifest file is missing the AssetBundleManifest asset.")]
ManifestAssetMissing,
[NameAndDescription("AssetBundleManifest Load Error",
"There was an error loading the AssetBundleManifest asset.")]
ManifestAssetLoadError,
[NameAndDescription("AssetBundle Files Missing", "This folder contains no AssetBundle files.")]
AssetBundlesMissing,
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 0110294bad4d544c486274699623a0c2
timeCreated: 1548906103
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
// 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.
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Provides a <see cref="Registry"/> of <see cref="IAssetPackValidator"/>s.
/// </summary>
public class AssetPackValidatorRegistry : Registry<IAssetPackValidator>
{
private static AssetPackValidatorRegistry _instance;
public static AssetPackValidatorRegistry Registry
{
get
{
if (_instance == null)
{
_instance = new AssetPackValidatorRegistry();
}
return _instance;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4ad448820c8d482996636e3862c1a420
timeCreated: 1574899929

View File

@@ -0,0 +1,29 @@
// 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.Collections.Generic;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Provides methods for validating whether an <see cref="AssetBundlePack"/> can be included in an app bundle.
/// </summary>
public interface IAssetPackValidator
{
/// <summary>
/// Returns a list of errors that would prevent the specified asset pack from being included in an app bundle.
/// </summary>
IList<AssetPackError> CheckBuildErrors(AssetBundlePack assetBundlePack);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 56526b5a57974a7f8d23788f47d6a0db
timeCreated: 1574729323

View File

@@ -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.Text.RegularExpressions;
using Google.Android.AppBundle.Editor.Internal.BuildTools;
using UnityEditor;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.AssetPacks
{
/// <summary>
/// Tools for adding texture compression format targeting to folders of an Android App Bundle.
/// </summary>
public static class TextureTargetingTools
{
/// <summary>
/// The key used by bundletool to recognize a folder targeted for a specific texture format
/// (for example, "tcf" in "textures#tcf_astc").
/// </summary>
private const string TextureCompressionFormatTargetingKey = "tcf";
/// <summary>
/// Returns the string used to designate a texture compression format in bundletool.
/// </summary>
public static string GetBundleToolTextureCompressionFormatName(TextureCompressionFormat format)
{
if (format == TextureCompressionFormat.Default)
{
return "";
}
var name = Enum.GetName(typeof(TextureCompressionFormat), format);
return name == null ? format.ToString() : name.ToLower();
}
/// <summary>
/// Get the targeting suffix for a given texture compression format, to
/// be appended to a folder name containing AssetBundles.
/// </summary>
public static string GetTargetingSuffix(TextureCompressionFormat format)
{
if (format == TextureCompressionFormat.Default)
{
return "";
}
return string.Format("#{0}_{1}", TextureCompressionFormatTargetingKey,
GetBundleToolTextureCompressionFormatName(format));
}
/// <summary>
/// Parse the folder name for a texture compression format, if any, and return it as
/// well as the name of the folder without the targeting.
/// </summary>
public static void GetTextureCompressionFormatAndStripSuffix(string folderName,
out TextureCompressionFormat targeting,
out string tcfStrippedFolderName)
{
targeting = TextureCompressionFormat.Default;
tcfStrippedFolderName = folderName;
// Parse the texture compression format used, if any, using the same convention
// as bundletool for folder suffixes.
Regex regex = new Regex(@"(?<base>.+?)#tcf_(?<value>.+)");
GroupCollection matchedGroups = regex.Match(folderName).Groups;
if (!matchedGroups["base"].Success || !matchedGroups["value"].Success)
{
// No texture compression format targeting found.
return;
}
string tcfValue = matchedGroups["value"].Captures[0].Value;
try
{
targeting = (TextureCompressionFormat) Enum.Parse(typeof(TextureCompressionFormat), tcfValue, true);
tcfStrippedFolderName = matchedGroups["base"].Captures[0].Value;
}
catch (ArgumentException)
{
Debug.LogWarningFormat(
"Ignoring unrecognized texture format \"{0}\" for folder: {1}",
tcfValue, folderName);
}
}
/// <summary>
/// Returns true if the specified SDK version is less than 21 and false if it's greater than or equal to 21.
///
/// This method can be used (along with the existence of install-time asset packs) to determine whether this
/// app requires <see cref="BundletoolConfig.StandaloneConfig"/> to support pre-L devices.
/// </summary>
public static bool IsSdkVersionPreLollipop(AndroidSdkVersions sdkVersion)
{
#if UNITY_2021_2_OR_NEWER
// The minimum API level supported by Unity 2021.2+ is 22.
return false;
#else
return sdkVersion < AndroidSdkVersions.AndroidApiLevel21;
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d474e3691efbb4141a8e8f46aeb406ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
// 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;
using System.IO;
using System.Linq;
using Google.Android.AppBundle.Editor.AssetPacks;
using Google.Android.AppBundle.Editor.Internal.BuildTools;
using UnityEditor;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// Helper to build and run the app based on the current build configuration.
/// </summary>
public static class BuildAndRunner
{
/// <summary>
/// Builds the app, then runs it on device.
/// First checks if any IBuildAndRunnerExtensions opt to override this action.
/// If so, it calls their BuildAndRun method.
/// Otherwise, it builds an AppBundle or apk based on asset delivery settings.
/// </summary>
public static void BuildAndRun()
{
// IBuildAndRunExtensions can opt to override the default Build and Run functionality.
var extensions = GetOverridingExtensions();
switch (extensions.Count)
{
case 0:
break;
case 1:
extensions.First().BuildAndRun();
return;
default:
// TODO(b/144588472): Choose one of multiple implementations instead of throwing.
throw new InvalidOperationException(
"Multiple IBuildExtensions attempting to override Build and Run.");
}
BuildAndRunDefault();
}
/// <summary>
/// First builds the current project as an APK or Android App Bundle, and then installs and runs it on a
/// connected Android device.
/// </summary>
private static void BuildAndRunDefault()
{
var assetPackConfig = AssetPackConfigSerializer.LoadConfig();
if (assetPackConfig.HasDeliveredAssetPacks())
{
AppBundlePublisher.BuildAndRun(assetPackConfig);
}
else
{
EmulateUnityBuildAndRun();
}
}
/// <summary>
/// Emulates Unity's File -> Build And Run menu option.
/// </summary>
private static void EmulateUnityBuildAndRun()
{
var androidSdk = new AndroidSdk();
var androidSdkPlatform = new AndroidSdkPlatform(androidSdk);
var androidBuilder = new AndroidBuilder(androidSdkPlatform);
var buildToolLogger = new BuildToolLogger();
if (!androidBuilder.Initialize(buildToolLogger))
{
return;
}
var artifactName = EditorUserBuildSettings.buildAppBundle ? "temp.aab" : "temp.apk";
var artifactPath = Path.Combine(Path.GetTempPath(), artifactName);
var buildPlayerOptions = AndroidBuildHelper.CreateBuildPlayerOptions(artifactPath);
buildPlayerOptions.options |= BuildOptions.AutoRunPlayer;
androidBuilder.Build(buildPlayerOptions);
}
// Visible for testing
public static List<IBuildAndRunExtension> GetOverridingExtensions()
{
return GetExtensions().Where(x => x.ShouldOverride()).ToList();
}
private static IEnumerable<IBuildAndRunExtension> GetExtensions()
{
return AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes())
.Where(type =>
typeof(IBuildAndRunExtension).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IBuildAndRunExtension>();
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 0928808bcd00049ba8435d9cfe4f410c
timeCreated: 1540331104
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,157 @@
// 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.Config;
using UnityEditor;
using UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal
{
/// <summary>
/// A window for managing settings related to building an Android app for distribution on Google Play.
/// </summary>
public class BuildSettingsWindow : EditorWindow
{
public const string WindowTitle = "Play Build Settings";
private const int WindowMinWidth = 500;
private const int WindowMinHeight = 250;
private const int FieldWidth = 175;
private static BuildSettingsWindow _windowInstance;
private string _assetBundleManifestPath;
/// <summary>
/// Displays this window, creating it if necessary.
/// </summary>
public static void ShowWindow()
{
_windowInstance = (BuildSettingsWindow) GetWindow(typeof(BuildSettingsWindow), true, WindowTitle);
_windowInstance.minSize = new Vector2(WindowMinWidth, WindowMinHeight);
}
private void OnDestroy()
{
_windowInstance = null;
}
private void Awake()
{
ReadFromBuildConfiguration();
}
/// <summary>
/// Read and update the window with most recent build configuration values.
/// </summary>
void ReadFromBuildConfiguration()
{
_assetBundleManifestPath = AndroidBuildConfiguration.AssetBundleManifestPath;
}
/// <summary>
/// Update window with most recent build configuration values if the window is open.
/// </summary>
public static void UpdateWindowIfOpen()
{
if (_windowInstance != null)
{
_windowInstance.ReadFromBuildConfiguration();
_windowInstance.Repaint();
}
}
private void OnGUI()
{
// Edge case that takes place when the plugin code gets re-compiled while this window is open.
if (_windowInstance == null)
{
_windowInstance = this;
}
var descriptionTextStyle = new GUIStyle(GUI.skin.label)
{
fontStyle = FontStyle.Italic,
wordWrap = true
};
EditorGUILayout.LabelField("Scenes in Build", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
string.Format(
"Note: the following settings affect APKs and app bundles built using the \"{0}\" menu's " +
"build actions, but not apps built using the \"File\" menu's build actions.",
GoogleEditorMenu.MainMenuName), descriptionTextStyle);
EditorGUILayout.Space();
EditorGUILayout.LabelField(
"The scenes in the build are selected via Unity's \"Build Settings\" window.", descriptionTextStyle);
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
var enabledScenes = string.Join(", ", AndroidBuildHelper.GetEditorBuildEnabledScenes());
EditorGUILayout.LabelField(string.Format("Scenes: {0}", enabledScenes), EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Update", GUILayout.Width(100)))
{
GetWindow(Type.GetType("UnityEditor.BuildPlayerWindow,UnityEditor"), true);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField(
"If you use AssetBundles, provide the path to your AssetBundle Manifest file below to ensure that " +
"required engine components are not stripped during the build process.", descriptionTextStyle);
EditorGUILayout.Space();
_assetBundleManifestPath =
GetLabelAndTextField("AssetBundle Manifest (Optional)", _assetBundleManifestPath);
EditorGUILayout.Space();
// Disable the Save button unless one of the fields has changed.
GUI.enabled = IsAnyFieldChanged();
if (GUILayout.Button("Save"))
{
SaveConfiguration();
}
GUI.enabled = true;
}
private bool IsAnyFieldChanged()
{
return _assetBundleManifestPath != AndroidBuildConfiguration.AssetBundleManifestPath;
}
private static string GetLabelAndTextField(string label, string text)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(label, GUILayout.Width(FieldWidth));
var result = EditorGUILayout.TextField(text);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
return result;
}
private void SaveConfiguration()
{
_assetBundleManifestPath = _assetBundleManifestPath.Trim();
AndroidBuildConfiguration.SaveConfiguration(_assetBundleManifestPath);
Debug.Log("Saved Android Build Settings");
// If a TextField is in focus, it won't update to reflect the Trim(). So reassign focus to controlID 0.
GUIUtility.keyboardControl = 0;
Repaint();
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d3494e8040ba74ee78ff64f1018b5969
timeCreated: 1540331105
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ee7c5073444e433eb10f83395460280
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: c349f457532c645d8817661cbdcb5729
timeCreated: 1556911730
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 46ae92b3dd8eb4e38b66ff3b58205318
timeCreated: 1556911730
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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&lt;AndroidBuildReport&gt; 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
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 24e34b4393a4d4a5d8de83ca5e08621c
timeCreated: 1556919494
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 3a5aaac6c5d78491aa3590cbff08a890
timeCreated: 1556913938
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 408798f1df959420d8fb95911a935f2d
timeCreated: 1556899571
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 36e42f83ed7a745ea9e4f4166d786141
timeCreated: 1556910452
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 960e36d9911aa4011958de1e7407757d
timeCreated: 1556916021
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a4ee87517b6d8405f9af0cd4e6d2029e
timeCreated: 1556919555
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8103f73350aca4529af9e65605055a8b
timeCreated: 1540331104
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1fb883fe4e14d4d90a8d664e93cb0b81
timeCreated: 1556899571
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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))
{
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 09bfd5773b8db4b3ebc596a1c11cae96
timeCreated: 1556899570
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f770d51f7cfa14c9da2a31e08a167e73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0996c5b6f76c44bab82db6c1f36a84c1
timeCreated: 1575644561

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: cc0e37b8322b54a4da00fdca472c4bb8
timeCreated: 1556913938
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6ed1ed129773a49ad9caa331244333ff
timeCreated: 1556899571
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bf519927705149098aece6a0c73137fe
timeCreated: 1594212511

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 16791a24cc9874bc6a41407b887146f5
timeCreated: 1556899571
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 70edb5163b984f9799e762ef659dabe6
timeCreated: 1557153210

View File

@@ -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();
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8e6ec7abd981a4580b1ec0a21602c072
timeCreated: 1558031253
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a72ad9ee7052447e385d166772c9b8ac
timeCreated: 1556921099
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: bed5ac126295a479887b763b3778752f
timeCreated: 1556919494
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
// Copyright 2021 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
{
/// <summary>
/// Provides methods for identifying if a project is configured to use Unity's built-in Play Asset Delivery support for
/// building Android App Bundles with asset packs.
/// </summary>
public static class BuiltInPadHelper
{
private const string AndroidPackDirectorySuffix = ".androidpack";
/// <summary>
/// Returns true if building the project with Unity's build system will produce an Android App Bundle with asset packs.
/// </summary>
public static bool ProjectUsesBuiltInPad()
{
return EditorSupportsPad() && EditorUserBuildSettings.buildAppBundle &&
(ProjectHasAndroidPacks() || PlayerSettings.Android.splitApplicationBinary);
}
/// <summary>
/// True if the project has a mainTemplate.gradle file in its Assets/Plugins/Android folder.
/// </summary>
public static bool ProjectUsesGradleTemplate()
{
return File.Exists(Path.Combine(Application.dataPath, "Plugins/Android/mainTemplate.gradle"));
}
/// <summary>
/// Returns true if the project contains folders ending in ".androidpack".
/// The .androidpack folder may optionally contain a build.gradle specifying the delivery mode.
/// </summary>
public static bool ProjectHasAndroidPacks()
{
// AssetDatabase.FindAssets is faster than searching the directories directly (e.g. Directory.EnumerateDirectories).
var androidPackGuids = AssetDatabase.FindAssets(AndroidPackDirectorySuffix);
foreach (var androidPackGuid in androidPackGuids)
{
var androidPackPath = AssetDatabase.GUIDToAssetPath(androidPackGuid);
if (androidPackPath.EndsWith(AndroidPackDirectorySuffix))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns true if the current version of Unity includes built-in support for building Android App Bundles with asset packs.
/// </summary>
public static bool EditorSupportsPad()
{
#if UNITY_2021_1_0 || UNITY_2021_1_1 || UNITY_2021_1_2 || UNITY_2021_1_3 || UNITY_2021_1_4 || UNITY_2021_1_5 || UNITY_2021_1_6 || UNITY_2021_1_7 || UNITY_2021_1_8 || UNITY_2021_1_9 || UNITY_2021_1_10 || UNITY_2021_1_11 || UNITY_2021_1_12 || UNITY_2021_1_13 || UNITY_2021_1_14
// Unity 2021 got built-in PAD support in version 2021.1.15
return false;
#elif UNITY_2020_1 || UNITY_2020_2 || UNITY_2020_3_0 || UNITY_2020_3_1 || UNITY_2020_3_2 || UNITY_2020_3_3 || UNITY_2020_3_4 || UNITY_2020_3_5 || UNITY_2020_3_6 || UNITY_2020_3_7 || UNITY_2020_3_8 || UNITY_2020_3_9 || UNITY_2020_3_10 || UNITY_2020_3_11 || UNITY_2020_3_12 || UNITY_2020_3_13 || UNITY_2020_3_14
// Unity 2020 got built-in PAD support in version 2020.3.15
return false;
#elif UNITY_2019_1|| UNITY_2019_2 || UNITY_2019_3 || UNITY_2019_4_0 || UNITY_2019_4_1 || UNITY_2019_4_2 || UNITY_2019_4_3 || UNITY_2019_4_4 || UNITY_2019_4_5 || UNITY_2019_4_6 || UNITY_2019_4_7 || UNITY_2019_4_8 || UNITY_2019_4_9 || UNITY_2019_4_10 || UNITY_2019_4_11 || UNITY_2019_4_12 || UNITY_2019_4_13 || UNITY_2019_4_14 || UNITY_2019_4_15 || UNITY_2019_4_16 || UNITY_2019_4_17 || UNITY_2019_4_18 || UNITY_2019_4_19 || UNITY_2019_4_20 || UNITY_2019_4_21 || UNITY_2019_4_22 || UNITY_2019_4_23 || UNITY_2019_4_24 || UNITY_2019_4_25 || UNITY_2019_4_26 || UNITY_2019_4_27 || UNITY_2019_4_28
// Unity 2019 got built-in PAD support in version 2019.4.29
return false;
#elif !UNITY_2019_1_OR_NEWER
// Unity versions 2018 and below do not have built-in PAD support
return false;
#else
return true;
#endif
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 871b3f388b424b83bf6946eb0fc7a707
timeCreated: 1633386322

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: acb206a54e25249ee9a2cbc373a4f4d3
folderAsset: yes
timeCreated: 1582764639
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,93 @@
// 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 UnityEngine;
namespace Google.Android.AppBundle.Editor.Internal.Config
{
/// <summary>
/// Provides methods for accessing and persisting build settings.
/// </summary>
public static class AndroidBuildConfiguration
{
/// <summary>
/// Configuration class that is serialized as JSON. See associated properties for field documentation.
/// </summary>
[Serializable]
private class Configuration
{
public string assetBundleManifestPath;
}
private static readonly string ConfigurationFilePath = Path.Combine("Library", "AndroidBuildConfig.json");
// Holds an in-memory copy of configuration for quick access.
private static Configuration _config;
/// <summary>
/// Optional field used to prevent removal of required components when building with engine stripping.
/// <a href="https://docs.unity3d.com/ScriptReference/BuildPlayerOptions-assetBundleManifestPath.html"/>
/// Never null.
/// </summary>
public static string AssetBundleManifestPath
{
get
{
LoadConfigIfNecessary();
return _config.assetBundleManifestPath ?? string.Empty;
}
}
/// <summary>
/// Persists the specified configuration to disk.
/// </summary>
public static void SaveConfiguration(string assetBundleManifestPath)
{
_config = _config ?? new Configuration();
_config.assetBundleManifestPath = assetBundleManifestPath;
File.WriteAllText(ConfigurationFilePath, JsonUtility.ToJson(_config));
}
private static void LoadConfigIfNecessary()
{
if (_config != null)
{
return;
}
LoadConfig(ConfigurationFilePath);
_config = _config ?? new Configuration();
}
private static void LoadConfig(string path)
{
if (!File.Exists(path))
{
return;
}
try
{
var configurationJson = File.ReadAllText(path);
_config = JsonUtility.FromJson<Configuration>(configurationJson);
}
catch (Exception ex)
{
Debug.LogErrorFormat("Failed to load {0} due to exception: {1}", path, ex);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 98657445555e4adab8532ed065e002f2
timeCreated: 1573690645

View File

@@ -0,0 +1,42 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.Config
{
[Serializable]
public class SerializableAssetBundle
{
public string path;
public string textureCompressionFormat = TextureCompressionFormat.Default.ToString();
public string deviceTier;
public TextureCompressionFormat TextureCompressionFormat
{
get { return SerializationHelper.GetTextureCompressionFormat(textureCompressionFormat); }
set { textureCompressionFormat = value.ToString(); }
}
public DeviceTier DeviceTier
{
get { return SerializationHelper.GetDeviceTier(deviceTier); }
set { deviceTier = value != null ? value.ToString() : null; }
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2971ccd90fecb4132ab5f746fce97ac4
timeCreated: 1582764639
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.Config
{
[Serializable]
public class SerializableAssetPack
{
public string name;
public string path;
public string deliveryMode = AssetPackDeliveryMode.DoNotPackage.ToString();
public AssetPackDeliveryMode DeliveryMode
{
get { return SerializationHelper.GetAssetPackDeliveryMode(deliveryMode); }
set { deliveryMode = value.ToString(); }
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d031fc28b0b84451ca9f735acf2cf991
timeCreated: 1582764640
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.Config
{
[Serializable]
public class SerializableAssetPackConfig
{
public List<SerializableAssetPack> assetPacks = new List<SerializableAssetPack>();
public List<SerializableMultiTargetingAssetPack> targetedAssetPacks =
new List<SerializableMultiTargetingAssetPack>();
public List<SerializableMultiTargetingAssetBundle> assetBundles =
new List<SerializableMultiTargetingAssetBundle>();
public bool splitBaseModuleAssets;
public string defaultTextureCompressionFormat = TextureCompressionFormat.Default.ToString();
public int defaultDeviceTier = 0;
public TextureCompressionFormat DefaultTextureCompressionFormat
{
get { return SerializationHelper.GetTextureCompressionFormat(defaultTextureCompressionFormat); }
set { defaultTextureCompressionFormat = value.ToString(); }
}
public DeviceTier DefaultDeviceTier
{
get { return DeviceTier.From(defaultDeviceTier); }
set { defaultDeviceTier = value.Tier; }
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: fdc1301dfd6c941c9bb32b684806e673
timeCreated: 1582764640
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
// 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;
namespace Google.Android.AppBundle.Editor.Internal.Config
{
[Serializable]
public class SerializableMultiTargetingAssetBundle
{
public string name;
public List<SerializableAssetBundle> assetBundles = new List<SerializableAssetBundle>();
public string deliveryMode = AssetPackDeliveryMode.DoNotPackage.ToString();
public AssetPackDeliveryMode DeliveryMode
{
get { return SerializationHelper.GetAssetPackDeliveryMode(deliveryMode); }
set { deliveryMode = value.ToString(); }
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 75b007f0f1ad54854939ae9055151cc4
timeCreated: 1582764640
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More