EdgegapBuildUtils.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Runtime.InteropServices;
  8. using System.Text.RegularExpressions;
  9. using System.Threading.Tasks;
  10. using UnityEditor;
  11. using UnityEditor.Build.Reporting;
  12. using Debug = UnityEngine.Debug;
  13. namespace Edgegap
  14. {
  15. internal static class EdgegapBuildUtils
  16. {
  17. public static bool IsArmCPU() =>
  18. RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
  19. RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
  20. public static BuildReport BuildServer()
  21. {
  22. IEnumerable<string> scenes = EditorBuildSettings.scenes.Select(s=>s.path);
  23. BuildPlayerOptions options = new BuildPlayerOptions
  24. {
  25. scenes = scenes.ToArray(),
  26. target = BuildTarget.StandaloneLinux64,
  27. // MIRROR CHANGE
  28. #if UNITY_2021_3_OR_NEWER
  29. subtarget = (int)StandaloneBuildSubtarget.Server, // dedicated server with UNITY_SERVER define
  30. #else
  31. options = BuildOptions.EnableHeadlessMode, // obsolete and missing UNITY_SERVER define
  32. #endif
  33. // END MIRROR CHANGE
  34. locationPathName = "Builds/EdgegapServer/ServerBuild"
  35. };
  36. return BuildPipeline.BuildPlayer(options);
  37. }
  38. public static async Task<bool> DockerSetupAndInstallationCheck()
  39. {
  40. if (!File.Exists("Dockerfile"))
  41. {
  42. File.WriteAllText("Dockerfile", dockerFileText);
  43. }
  44. string output = null;
  45. string error = null;
  46. await RunCommand_DockerVersion(msg => output = msg, msg => error = msg); // MIRROR CHANGE
  47. if (!string.IsNullOrEmpty(error))
  48. {
  49. Debug.LogError(error);
  50. return false;
  51. }
  52. Debug.Log($"[Edgegap] Docker version detected: {output}"); // MIRROR CHANGE
  53. return true;
  54. }
  55. // MIRROR CHANGE
  56. static async Task RunCommand_DockerVersion(Action<string> outputReciever = null, Action<string> errorReciever = null)
  57. {
  58. #if UNITY_EDITOR_WIN
  59. await RunCommand("cmd.exe", "/c docker --version", outputReciever, errorReciever);
  60. #elif UNITY_EDITOR_OSX
  61. await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever);
  62. #elif UNITY_EDITOR_LINUX
  63. await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever);
  64. #else
  65. Debug.LogError("The platform is not supported yet.");
  66. #endif
  67. }
  68. // MIRROR CHANGE
  69. public static async Task RunCommand_DockerBuild(string registry, string imageRepo, string tag, Action<string> onStatusUpdate)
  70. {
  71. string realErrorMessage = null;
  72. // ARM -> x86 support:
  73. // build commands use 'buildx' on ARM cpus for cross compilation.
  74. // otherwise docker builds would not launch when deployed because
  75. // Edgegap's infrastructure is on x86. instead the deployment logs
  76. // would show an error in a linux .go file with 'not found'.
  77. string buildCommand = IsArmCPU() ? "buildx build --platform linux/amd64" : "build";
  78. #if UNITY_EDITOR_WIN
  79. await RunCommand("docker.exe", $"{buildCommand} -t {registry}/{imageRepo}:{tag} .", onStatusUpdate,
  80. #elif UNITY_EDITOR_OSX
  81. await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate,
  82. #elif UNITY_EDITOR_LINUX
  83. await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate,
  84. #endif
  85. (msg) =>
  86. {
  87. if (msg.Contains("ERROR"))
  88. {
  89. realErrorMessage = msg;
  90. }
  91. onStatusUpdate(msg);
  92. });
  93. if(realErrorMessage != null)
  94. {
  95. throw new Exception(realErrorMessage);
  96. }
  97. }
  98. public static async Task<(bool, string)> RunCommand_DockerPush(string registry, string imageRepo, string tag, Action<string> onStatusUpdate)
  99. {
  100. string error = string.Empty;
  101. #if UNITY_EDITOR_WIN
  102. await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, (msg) => error += msg + "\n");
  103. #elif UNITY_EDITOR_OSX
  104. await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n");
  105. #elif UNITY_EDITOR_LINUX
  106. await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n");
  107. #endif
  108. if (!string.IsNullOrEmpty(error))
  109. {
  110. Debug.LogError(error);
  111. return (false, error);
  112. }
  113. return (true, null);
  114. }
  115. // END MIRROR CHANGE
  116. static async Task RunCommand(string command, string arguments, Action<string> outputReciever = null, Action<string> errorReciever = null)
  117. {
  118. ProcessStartInfo startInfo = new ProcessStartInfo()
  119. {
  120. FileName = command,
  121. Arguments = arguments,
  122. RedirectStandardOutput = true,
  123. RedirectStandardError = true,
  124. UseShellExecute = false,
  125. CreateNoWindow = true,
  126. };
  127. // MIRROR CHANGE
  128. #if !UNITY_EDITOR_WIN
  129. // on mac, commands like 'docker' aren't found because it's not in the application's PATH
  130. // even if it runs on mac's terminal.
  131. // to solve this we need to do two steps:
  132. // 1. add /usr/bin/local to PATH if it's not there already. often this is missing in the application.
  133. // this is where docker is usually instaled.
  134. // 2. add PATH to ProcessStartInfo
  135. string existingPath = Environment.GetEnvironmentVariable("PATH");
  136. string customPath = $"{existingPath}:/usr/local/bin";
  137. startInfo.EnvironmentVariables["PATH"] = customPath;
  138. // Debug.Log("PATH: " + customPath);
  139. #endif
  140. // END MIRROR CHANGE
  141. Process proc = new Process() { StartInfo = startInfo, };
  142. proc.EnableRaisingEvents = true;
  143. ConcurrentQueue<string> errors = new ConcurrentQueue<string>();
  144. ConcurrentQueue<string> outputs = new ConcurrentQueue<string>();
  145. void pipeQueue(ConcurrentQueue<string> q, Action<string> opt)
  146. {
  147. while (!q.IsEmpty)
  148. {
  149. if (q.TryDequeue(out string msg) && !string.IsNullOrWhiteSpace(msg))
  150. {
  151. opt?.Invoke(msg);
  152. }
  153. }
  154. }
  155. proc.OutputDataReceived += (s, e) => outputs.Enqueue(e.Data);
  156. proc.ErrorDataReceived += (s, e) => errors.Enqueue(e.Data);
  157. proc.Start();
  158. proc.BeginOutputReadLine();
  159. proc.BeginErrorReadLine();
  160. while (!proc.HasExited)
  161. {
  162. await Task.Delay(100);
  163. pipeQueue(errors, errorReciever);
  164. pipeQueue(outputs, outputReciever);
  165. }
  166. pipeQueue(errors, errorReciever);
  167. pipeQueue(outputs, outputReciever);
  168. }
  169. static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
  170. {
  171. throw new NotImplementedException();
  172. }
  173. static Regex lastDigitsRegex = new Regex("([0-9])+$");
  174. public static string IncrementTag(string tag)
  175. {
  176. Match lastDigits = lastDigitsRegex.Match(tag);
  177. if (!lastDigits.Success)
  178. {
  179. return tag + " _1";
  180. }
  181. int number = int.Parse(lastDigits.Groups[0].Value);
  182. number++;
  183. return lastDigitsRegex.Replace(tag, number.ToString());
  184. }
  185. public static void UpdateEdgegapAppTag(string tag)
  186. {
  187. // throw new NotImplementedException();
  188. }
  189. // -batchmode -nographics remains for Unity 2019/2020 support pre-dedicated server builds
  190. static string dockerFileText = @"FROM ubuntu:bionic
  191. ARG DEBIAN_FRONTEND=noninteractive
  192. COPY Builds/EdgegapServer /root/build/
  193. WORKDIR /root/
  194. RUN chmod +x /root/build/ServerBuild
  195. ENTRYPOINT [ ""/root/build/ServerBuild"", ""-batchmode"", ""-nographics""]
  196. ";
  197. /// <summary>Run a Docker cmd with streaming log response. TODO: Plugin to other Docker cmds</summary>
  198. /// <returns>Throws if logs contain "ERROR"</returns>
  199. ///
  200. /// <param name="registryUrl">ex: "registry.edgegap.com"</param>
  201. /// <param name="repoUsername">ex: "robot$mycompany-asdf+client-push"</param>
  202. /// <param name="repoPasswordToken">Different from ApiToken; sometimes called "Container Registry Password"</param>
  203. /// <param name="onStatusUpdate">Log stream</param>
  204. // MIRROR CHANGE: CROSS PLATFORM SUPPORT
  205. static async Task<bool> RunCommand_DockerLogin(
  206. string registryUrl,
  207. string repoUsername,
  208. string repoPasswordToken,
  209. Action<string> outputReciever = null, Action<string> errorReciever = null)
  210. {
  211. // TODO: Use --password-stdin for security (!) This is no easy task for child Process | https://stackoverflow.com/q/51489359/6541639
  212. // (!) Don't use single quotes for cross-platform support (works unexpectedly in `cmd`).
  213. try
  214. {
  215. #if UNITY_EDITOR_WIN
  216. await RunCommand("cmd.exe", $"/c docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"", outputReciever, errorReciever);
  217. #elif UNITY_EDITOR_OSX
  218. await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever);
  219. #elif UNITY_EDITOR_LINUX
  220. await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever);
  221. #else
  222. Debug.LogError("The platform is not supported yet.");
  223. #endif
  224. }
  225. catch (Exception e)
  226. {
  227. Debug.LogError($"Error: {e}");
  228. return false;
  229. }
  230. return true;
  231. }
  232. /// <summary>
  233. /// v2: Login to Docker Registry via RunCommand(), returning streamed log messages:
  234. /// "docker login {registryUrl} {repository} {repoUsername} {repoPasswordToken}"
  235. /// </summary>
  236. /// <param name="registryUrl">ex: "registry.edgegap.com"</param>
  237. /// <param name="repoUsername">ex: "robot$mycompany-asdf+client-push"</param>
  238. /// <param name="repoPasswordToken">Different from ApiToken; sometimes called "Container Registry Password"</param>
  239. /// <param name="onStatusUpdate">Log stream</param>
  240. /// <returns>isSuccess</returns>
  241. public static async Task<bool> LoginContainerRegistry(
  242. string registryUrl,
  243. string repoUsername,
  244. string repoPasswordToken,
  245. Action<string> onStatusUpdate)
  246. {
  247. string error = null;
  248. await RunCommand_DockerLogin(registryUrl, repoUsername, repoPasswordToken, onStatusUpdate, msg => error = msg); // MIRROR CHANGE
  249. if (error.Contains("ERROR"))
  250. {
  251. Debug.LogError(error);
  252. return false;
  253. }
  254. return true;
  255. }
  256. }
  257. }