using System;
using System.Collections;
using System.IO;
using System.Linq;
using Unity.EditorCoroutines.Editor;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.SettingsManagement;
using UnityEngine;
namespace Unity.Play.Publisher.Editor
{
class PublisherBuildProcessor : IPostprocessBuildWithReport, IPreprocessBuildWithReport
{
const string DEFAULT_BUILDS_FOLDER = "WebGL Builds";
///
/// Path to the folder proposed as the default location for builds
///
public static readonly string DefaultBuildsFolderPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, PublisherBuildProcessor.DEFAULT_BUILDS_FOLDER);
///
/// Should a default folder be created and proposed for builds?
///
[UserSetting("Publish WebGL Game", "Create default build folder", "When enabled, a folder named '" + DEFAULT_BUILDS_FOLDER + "' will be created next to the Assets folder and used as the proposed location for new builds")]
public static UserSetting CreateDefaultBuildsFolder = new UserSetting(PublisherSettingsManager.instance, "createDefaultBuildsFolder", true, SettingsScope.Project);
static bool buildStartedFromTool = false;
///
/// The order in which the PostProcess and PreProcess builds are processed
///
public int callbackOrder { get { return 0; } }
///
/// Called right after a build process ends
///
/// A summary of the build process
public void OnPostprocessBuild(BuildReport report)
{
BuildSummary summary = report.summary;
if (summary.platform != BuildTarget.WebGL) { return; }
string buildOutputDir = summary.outputPath;
string buildGUID = summary.guid.ToString();
PublisherUtils.AddBuildDirectory(buildOutputDir);
PublisherWindow windowInstance = PublisherWindow.FindInstance();
windowInstance?.Store.Dispatch(new BuildFinishAction
{
outputDir = buildOutputDir,
buildGUID = buildGUID
});
WriteMetadataFilesAndFinalizeBuild(summary.outputPath, buildGUID);
}
IEnumerator WaitUntilBuildFinishes(BuildReport report)
{
/* [NOTE] You might want to use a frame wait instead of a time based one:
* Building is main thread, and we won't get a frame update until the build is complete.
* So that would almost certainly wait until right after the build is done and next frame tick,
* reducing the likely hood of data being unloaded / unavaliable due to
* cleanup operations which could happen to the build report as variables on the stack are not counted as "in use" for the GC system
*/
EditorWaitForSeconds waitForSeconds = new EditorWaitForSeconds(1f);
while (BuildPipeline.isBuildingPlayer)
{
yield return waitForSeconds;
}
AnalyticsHelper.BuildCompleted(report.summary.result, report.summary.totalTime);
switch (report.summary.result)
{
case BuildResult.Cancelled: Debug.LogWarning("[Version and Build] Build cancelled! " + report.summary.totalTime); break;
case BuildResult.Failed: Debug.LogError("[Version and Build] Build failed! " + report.summary.totalTime); break;
case BuildResult.Succeeded: Debug.Log("[Version and Build] Build succeeded! " + report.summary.totalTime); break;
case BuildResult.Unknown: Debug.Log("[Version and Build] Unknown build result! " + report.summary.totalTime); break;
}
}
static IEnumerator WritePackagesListAndFinalizeBuild(string dependenciesFilePath)
{
var request = UnityEditor.PackageManager.Client.List(false, false);
while (!request.IsCompleted)
{
yield return null;
}
string templatePackageID = GetTemplatePackageID();
if (string.IsNullOrEmpty(templatePackageID))
{
templatePackageID = $"{Application.productName}@{Application.version}";
}
using (StreamWriter streamWriter = new StreamWriter(dependenciesFilePath, false))
{
request.Result
.Select(pkg => $"{pkg.name}@{pkg.version}")
// We probably don't have the package.json of the used template available,
// so add the information manually
.Concat(new[] { templatePackageID })
.Distinct()
.ToList()
.ForEach(streamWriter.WriteLine);
}
PublisherWindow windowInstance = PublisherWindow.FindInstance();
windowInstance?.OnBuildCompleted(windowInstance.Store.state.buildOutputDir);
}
///
/// Gets the ID of the template packaged used in this project, reading it directly from the ProjectSettings.
///
/// Returns null if the value is not set
static string GetTemplatePackageID()
{
const string projectSettingsAssetPath = "ProjectSettings/ProjectSettings.asset";
SerializedObject projectSettings = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath(projectSettingsAssetPath)[0]);
return projectSettings.FindProperty("templatePackageId")?.stringValue;
}
///
/// Write metadata files into the build directory
///
///
void WriteMetadataFilesAndFinalizeBuild(string outputPath, string buildGUID)
{
try
{
// The Unity version used
string versionFilePath = $"{outputPath}/ProjectVersion.txt";
File.Copy("ProjectSettings/ProjectVersion.txt", versionFilePath, true);
string guidFilePath = $"{outputPath}/GUID.txt";
File.WriteAllText(guidFilePath, buildGUID);
// dependencies.txt: list of "depepedency@version"
string dependenciesFilePath = $"{outputPath}/dependencies.txt";
EditorCoroutineUtility.StartCoroutineOwnerless(WritePackagesListAndFinalizeBuild(dependenciesFilePath));
}
catch (Exception e)
{
Debug.LogException(e);
}
}
///
/// Triggers the "Build Game" dialog
///
/// True and the build path if everything goes well and the build is done, false and empty string otherwise.
public static (bool, string) OpenBuildGameDialog(BuildTarget activeBuildTarget)
{
string path = string.Empty;
try
{
string defaultOutputDirectory = PublisherUtils.GetFirstValidBuildPath();
if (string.IsNullOrEmpty(defaultOutputDirectory) && CreateDefaultBuildsFolder)
{
defaultOutputDirectory = DefaultBuildsFolderPath;
if (!Directory.Exists(defaultOutputDirectory))
{
Directory.CreateDirectory(defaultOutputDirectory);
}
}
path = EditorUtility.SaveFolderPanel(Localization.Tr("DIALOG_CHOOSE_BUILD_FOLDER"), defaultOutputDirectory, "");
if (string.IsNullOrEmpty(path)) { return (false, string.Empty); }
BuildPlayerOptions buildOptions = new BuildPlayerOptions();
buildOptions.scenes = EditorBuildSettingsScene.GetActiveSceneList(EditorBuildSettings.scenes);
buildOptions.locationPathName = path;
buildOptions.options = BuildOptions.None;
buildOptions.targetGroup = BuildPipeline.GetBuildTargetGroup(activeBuildTarget);
buildOptions.target = activeBuildTarget;
buildStartedFromTool = true;
BuildReport report = BuildPipeline.BuildPlayer(buildOptions);
buildStartedFromTool = false;
AnalyticsHelper.BuildCompleted(report.summary.result, report.summary.totalTime);
switch (report.summary.result)
{
case BuildResult.Cancelled: //Debug.LogWarning("[Version and Build] Build cancelled! " + report.summary.totalTime);
case BuildResult.Failed: //Debug.LogError("[Version and Build] Build failed! " + report.summary.totalTime);
return (false, string.Empty);
case BuildResult.Succeeded: //Debug.Log("[Version and Build] Build succeeded! " + report.summary.totalTime);
case BuildResult.Unknown: //Debug.Log("[Version and Build] Unknown build result! " + report.summary.totalTime);
break;
}
}
catch (BuildPlayerWindow.BuildMethodException /*e*/)
{
//Debug.LogError(e.Message);
return (false, string.Empty);
}
return (true, path);
}
///
/// Called right before the build process starts
///
/// A summary of the build process
public void OnPreprocessBuild(BuildReport report)
{
if (report.summary.platform != BuildTarget.WebGL) { return; }
AnalyticsHelper.BuildStarted(buildStartedFromTool);
if (buildStartedFromTool) { return; }
//then we need to wait until the build process finishes, in order to get the proper BuildReport
EditorCoroutineUtility.StartCoroutineOwnerless(WaitUntilBuildFinishes(report));
}
}
}