slowpoker/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs
2024-10-17 17:23:05 +03:00

299 lines
12 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Build.Reporting;
using Debug = UnityEngine.Debug;
namespace Edgegap
{
internal static class EdgegapBuildUtils
{
public static bool IsArmCPU() =>
RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
public static BuildReport BuildServer()
{
IEnumerable<string> scenes = EditorBuildSettings.scenes.Select(s=>s.path);
BuildPlayerOptions options = new BuildPlayerOptions
{
scenes = scenes.ToArray(),
target = BuildTarget.StandaloneLinux64,
// MIRROR CHANGE
#if UNITY_2021_3_OR_NEWER
subtarget = (int)StandaloneBuildSubtarget.Server, // dedicated server with UNITY_SERVER define
#else
options = BuildOptions.EnableHeadlessMode, // obsolete and missing UNITY_SERVER define
#endif
// END MIRROR CHANGE
locationPathName = "Builds/EdgegapServer/ServerBuild"
};
return BuildPipeline.BuildPlayer(options);
}
public static async Task<bool> DockerSetupAndInstallationCheck()
{
if (!File.Exists("Dockerfile"))
{
File.WriteAllText("Dockerfile", dockerFileText);
}
string output = null;
string error = null;
await RunCommand_DockerVersion(msg => output = msg, msg => error = msg); // MIRROR CHANGE
if (!string.IsNullOrEmpty(error))
{
Debug.LogError(error);
return false;
}
Debug.Log($"[Edgegap] Docker version detected: {output}"); // MIRROR CHANGE
return true;
}
// MIRROR CHANGE
static async Task RunCommand_DockerVersion(Action<string> outputReciever = null, Action<string> errorReciever = null)
{
#if UNITY_EDITOR_WIN
await RunCommand("cmd.exe", "/c docker --version", outputReciever, errorReciever);
#elif UNITY_EDITOR_OSX
await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever);
#elif UNITY_EDITOR_LINUX
await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever);
#else
Debug.LogError("The platform is not supported yet.");
#endif
}
// MIRROR CHANGE
public static async Task RunCommand_DockerBuild(string registry, string imageRepo, string tag, Action<string> onStatusUpdate)
{
string realErrorMessage = null;
// ARM -> x86 support:
// build commands use 'buildx' on ARM cpus for cross compilation.
// otherwise docker builds would not launch when deployed because
// Edgegap's infrastructure is on x86. instead the deployment logs
// would show an error in a linux .go file with 'not found'.
string buildCommand = IsArmCPU() ? "buildx build --platform linux/amd64" : "build";
#if UNITY_EDITOR_WIN
await RunCommand("docker.exe", $"{buildCommand} -t {registry}/{imageRepo}:{tag} .", onStatusUpdate,
#elif UNITY_EDITOR_OSX
await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate,
#elif UNITY_EDITOR_LINUX
await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate,
#endif
(msg) =>
{
if (msg.Contains("ERROR"))
{
realErrorMessage = msg;
}
onStatusUpdate(msg);
});
if(realErrorMessage != null)
{
throw new Exception(realErrorMessage);
}
}
public static async Task<(bool, string)> RunCommand_DockerPush(string registry, string imageRepo, string tag, Action<string> onStatusUpdate)
{
string error = string.Empty;
#if UNITY_EDITOR_WIN
await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, (msg) => error += msg + "\n");
#elif UNITY_EDITOR_OSX
await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n");
#elif UNITY_EDITOR_LINUX
await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n");
#endif
if (!string.IsNullOrEmpty(error))
{
Debug.LogError(error);
return (false, error);
}
return (true, null);
}
// END MIRROR CHANGE
static async Task RunCommand(string command, string arguments, Action<string> outputReciever = null, Action<string> errorReciever = null)
{
ProcessStartInfo startInfo = new ProcessStartInfo()
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
// MIRROR CHANGE
#if !UNITY_EDITOR_WIN
// on mac, commands like 'docker' aren't found because it's not in the application's PATH
// even if it runs on mac's terminal.
// to solve this we need to do two steps:
// 1. add /usr/bin/local to PATH if it's not there already. often this is missing in the application.
// this is where docker is usually instaled.
// 2. add PATH to ProcessStartInfo
string existingPath = Environment.GetEnvironmentVariable("PATH");
string customPath = $"{existingPath}:/usr/local/bin";
startInfo.EnvironmentVariables["PATH"] = customPath;
// Debug.Log("PATH: " + customPath);
#endif
// END MIRROR CHANGE
Process proc = new Process() { StartInfo = startInfo, };
proc.EnableRaisingEvents = true;
ConcurrentQueue<string> errors = new ConcurrentQueue<string>();
ConcurrentQueue<string> outputs = new ConcurrentQueue<string>();
void pipeQueue(ConcurrentQueue<string> q, Action<string> opt)
{
while (!q.IsEmpty)
{
if (q.TryDequeue(out string msg) && !string.IsNullOrWhiteSpace(msg))
{
opt?.Invoke(msg);
}
}
}
proc.OutputDataReceived += (s, e) => outputs.Enqueue(e.Data);
proc.ErrorDataReceived += (s, e) => errors.Enqueue(e.Data);
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
while (!proc.HasExited)
{
await Task.Delay(100);
pipeQueue(errors, errorReciever);
pipeQueue(outputs, outputReciever);
}
pipeQueue(errors, errorReciever);
pipeQueue(outputs, outputReciever);
}
static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
throw new NotImplementedException();
}
static Regex lastDigitsRegex = new Regex("([0-9])+$");
public static string IncrementTag(string tag)
{
Match lastDigits = lastDigitsRegex.Match(tag);
if (!lastDigits.Success)
{
return tag + " _1";
}
int number = int.Parse(lastDigits.Groups[0].Value);
number++;
return lastDigitsRegex.Replace(tag, number.ToString());
}
public static void UpdateEdgegapAppTag(string tag)
{
// throw new NotImplementedException();
}
// -batchmode -nographics remains for Unity 2019/2020 support pre-dedicated server builds
static string dockerFileText = @"FROM ubuntu:bionic
ARG DEBIAN_FRONTEND=noninteractive
COPY Builds/EdgegapServer /root/build/
WORKDIR /root/
RUN chmod +x /root/build/ServerBuild
ENTRYPOINT [ ""/root/build/ServerBuild"", ""-batchmode"", ""-nographics""]
";
/// <summary>Run a Docker cmd with streaming log response. TODO: Plugin to other Docker cmds</summary>
/// <returns>Throws if logs contain "ERROR"</returns>
///
/// <param name="registryUrl">ex: "registry.edgegap.com"</param>
/// <param name="repoUsername">ex: "robot$mycompany-asdf+client-push"</param>
/// <param name="repoPasswordToken">Different from ApiToken; sometimes called "Container Registry Password"</param>
/// <param name="onStatusUpdate">Log stream</param>
// MIRROR CHANGE: CROSS PLATFORM SUPPORT
static async Task<bool> RunCommand_DockerLogin(
string registryUrl,
string repoUsername,
string repoPasswordToken,
Action<string> outputReciever = null, Action<string> errorReciever = null)
{
// TODO: Use --password-stdin for security (!) This is no easy task for child Process | https://stackoverflow.com/q/51489359/6541639
// (!) Don't use single quotes for cross-platform support (works unexpectedly in `cmd`).
try
{
#if UNITY_EDITOR_WIN
await RunCommand("cmd.exe", $"/c docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"", outputReciever, errorReciever);
#elif UNITY_EDITOR_OSX
await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever);
#elif UNITY_EDITOR_LINUX
await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever);
#else
Debug.LogError("The platform is not supported yet.");
#endif
}
catch (Exception e)
{
Debug.LogError($"Error: {e}");
return false;
}
return true;
}
/// <summary>
/// v2: Login to Docker Registry via RunCommand(), returning streamed log messages:
/// "docker login {registryUrl} {repository} {repoUsername} {repoPasswordToken}"
/// </summary>
/// <param name="registryUrl">ex: "registry.edgegap.com"</param>
/// <param name="repoUsername">ex: "robot$mycompany-asdf+client-push"</param>
/// <param name="repoPasswordToken">Different from ApiToken; sometimes called "Container Registry Password"</param>
/// <param name="onStatusUpdate">Log stream</param>
/// <returns>isSuccess</returns>
public static async Task<bool> LoginContainerRegistry(
string registryUrl,
string repoUsername,
string repoPasswordToken,
Action<string> onStatusUpdate)
{
string error = null;
await RunCommand_DockerLogin(registryUrl, repoUsername, repoPasswordToken, onStatusUpdate, msg => error = msg); // MIRROR CHANGE
if (error.Contains("ERROR"))
{
Debug.LogError(error);
return false;
}
return true;
}
}
}