using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using UnityEngine; using UnityEditorInternal; using UnityEditor; using NiceIO.Sysroot; #if UNITY_STANDALONE_LINUX_API using UnityEditor.LinuxStandalone; #endif #if UNITY_EMBEDDED_LINUX_API using UnityEditor.EmbeddedLinux; #endif namespace UnityEditor.Il2Cpp { /// /// Describes where a sysroot/toolchain payload is located and where it should be installed. /// /// /// /// points to the compressed payload archive (typically /// Packages/<name>/data~/payload.tar.7z). /// /// /// is the destination directory under the sysroot cache where the /// archive will be extracted. /// /// public struct PayloadDescriptor { /// /// Absolute path to the payload tarball to install (e.g., payload.tar.7z). /// internal NPath path; /// /// Absolute destination directory where the payload will be extracted. /// internal NPath dir; } /// /// Tracks one-time initialization state for a package. /// enum InitializationStatus { /// Initialization has not been attempted. Uninitialized, /// Initialization was attempted and failed. Failed, /// Initialization completed successfully. Succeeded } /// /// Base class for sysroot and toolchain packages. /// /// /// When the UNITY_STANDALONE_LINUX_API or UNITY_EMBEDDED_LINUX_API defines are set, /// this class implements the interface consumed by the Linux build pipeline. /// Derived classes supply platform/arch metadata and the payload locations needed by IL2CPP. /// public class SysrootPackage #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API : Sysroot #endif { /// /// Heuristically checks whether the Linux IL2CPP Players are present in the Editor installation. /// /// /// Used to emit a helpful debug warning when sysroot/toolchain packages are present but /// the IL2CPP player toolchain for Linux is missing. /// private static bool IsLinuxIL2CPPPresent() { string targetDir = $"{BuildPipeline.GetPlaybackEngineDirectory(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64, BuildOptions.None)}/Variations/il2cpp"; if (Directory.Exists(targetDir)) return true; return false; } /// /// Returns when verbose debug logging for sysroots is enabled. /// /// /// Controlled by the UNITY_SYSROOT_DEBUG environment variable (any non-empty value). /// /// /// if UNITY_SYSROOT_DEBUG is set to a non-empty string; /// otherwise, . /// public static bool ShouldLogDebugMessage() { return !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("UNITY_SYSROOT_DEBUG")); } /// /// On editor load, warns (in debug mode) if Linux IL2CPP is missing while Linux toolchain packages are present. /// [InitializeOnLoadMethod] private static void IssueWarningIfLinuxIL2CPPNotPresent() { if (ShouldLogDebugMessage() && !IsLinuxIL2CPPPresent()) { UnityEngine.Debug.LogWarning($"Linux Compiler Toolchain package(s) present, but required Linux-IL2CPP is missing"); } } /// /// Package identifier (UPM name). /// public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string Name => "com.unity.sysroot.base"; /// /// Host operating system (e.g., linux, win, macos). /// /// /// Base implementation returns an empty string; derived classes should override with a stable token. /// public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string HostPlatform => ""; /// /// Host CPU architecture (e.g., x86_64, arm64). /// /// Base implementation returns an empty string; override in derived packages. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string HostArch => ""; /// /// Target operating system (e.g., linux, win, macos). /// /// Base implementation returns an empty string; override in derived packages. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string TargetPlatform => ""; /// /// Target CPU architecture (e.g., x86_64, arm64). /// /// Base implementation returns an empty string; override in derived packages. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string TargetArch => ""; /// /// Additional arguments to append to the IL2CPP invocation. /// /// /// Return null for no extra arguments. Derived classes can yield a sequence of /// flags (e.g., --sysroot=<path>) that IL2CPP will consume. /// /// Enumerable of additional arguments, or null. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif IEnumerable GetIl2CppArguments() { return null; } /// /// Absolute path to the installed sysroot to be used by IL2CPP. /// /// /// Return null if this package does not supply a sysroot (e.g., toolchain-only packages). /// /// Sysroot directory path, or null. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string GetSysrootPath() { return null; } /// /// Absolute path to the installed toolchain to be used by IL2CPP. /// /// /// Return null if this package does not supply a toolchain (e.g., sysroot-only packages). /// /// Toolchain directory path, or null. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string GetToolchainPath() { return null; } /// /// Extra compiler flags (passed to Clang via IL2CPP) required by this package. /// /// /// Typical use is to supply a target triple (e.g., -target aarch64-unity-linux-gnu) /// or sysroot/emulation flags. Return null for none. /// /// Space-separated compiler flags, or null. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string GetIl2CppCompilerFlags() { return null; } /// /// Extra linker flags (passed via IL2CPP) required by this package. /// /// /// Usually mirrors compiler targeting flags so the linker resolves from the same sysroot. /// Return null for none. /// /// Space-separated linker flags, or null. public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif string GetIl2CppLinkerFlags() { return null; } /// /// Default archive file name used by sysroot/toolchain packages. /// /// /// The file lives under the package’s data~ folder by convention (see ). /// protected string Payload => "payload.tar.7z"; private List _payloads = new List(); private InitializationStatus _initStatus = InitializationStatus.Uninitialized; /// /// Ensures all registered payloads are installed. Safe to call multiple times. /// /// /// If a destination directory does not exist, the corresponding payload is extracted into it. /// On any failure, the method logs an error and returns . /// /// if initialization completed successfully; otherwise . public #if UNITY_STANDALONE_LINUX_API || UNITY_EMBEDDED_LINUX_API override #else virtual #endif bool Initialize() { if (_initStatus != InitializationStatus.Uninitialized) return _initStatus == InitializationStatus.Succeeded; foreach (PayloadDescriptor pd in _payloads) { if (!Directory.Exists(pd.dir.ToString(SlashMode.Native)) && !InstallPayload(pd)) { UnityEngine.Debug.LogError($"Failed to initialize package: {Name}"); _initStatus = InitializationStatus.Failed; return false; } } _initStatus = InitializationStatus.Succeeded; return true; } /// /// Computes the absolute path to the packaged payload archive inside a UPM package. /// /// UPM package name (e.g., com.unity.sdk.linux-arm64). /// Absolute path to data~/payload.tar.7z under the package root. internal NPath PayloadPath(string packageName) { return new NPath(Path.GetFullPath($"Packages/{packageName}")).Combine("data~/payload.tar.7z"); } /// /// Registers a payload tarball and its installation directory for later initialization. /// /// UPM package name that contains the payload. /// Destination directory relative to the sysroot cache (see ). public void RegisterPayload(string packageName, string payloadDir) { _payloads.Add(new PayloadDescriptor{path = PayloadPath(packageName).ToString(SlashMode.Native), dir = PayloadInstallDirectory(payloadDir).ToString(SlashMode.Native)}); } /// /// Executes a shell command synchronously. /// /// Command line to run. Quoting is handled by this method for platform differences. /// Optional working directory. Defaults to . /// if the command exits with code 0; otherwise . /// /// On Windows this runs under cmd /c; on Unix under /bin/sh -c. /// When is , failures are logged with command details. /// private bool RunShellCommand(string command, string workDir = null) { var p = new Process(); p.StartInfo.UseShellExecute = false; #if UNITY_EDITOR_WIN p.StartInfo.CreateNoWindow = true; p.StartInfo.FileName = "cmd"; p.StartInfo.Arguments = $"/c \"{command}\""; #else p.StartInfo.FileName = "/bin/sh"; p.StartInfo.Arguments = $"-c \'{command}\'"; #endif p.StartInfo.WorkingDirectory = string.IsNullOrEmpty(workDir) ? Environment.CurrentDirectory : workDir; p.Start(); p.WaitForExit(); bool result = p.ExitCode == 0; if (!result && ShouldLogDebugMessage()) UnityEngine.Debug.LogError($"Failed to execute command command=\"{p.StartInfo.FileName}\" arguments=\"{p.StartInfo.Arguments}\""); return result; } /// /// Creates the destination directory and extracts the payload into it. /// /// Path to the compressed tarball. /// Destination directory to extract to. /// on success; otherwise . /// /// On failure the partially created directory is cleaned up. Extraction uses 7-Zip on both platforms /// and pipes to tar on Unix. /// private bool DecompressSysroot(NPath payload, NPath workDir) { if (!RunShellCommand(CommandCreateDirectory(workDir))) { UnityEngine.Debug.LogError($"Failed to create directory {workDir}"); return false; } if (!RunShellCommand(CommandUncompressTarball(payload, workDir), workDir.ToString(SlashMode.Native))) { UnityEngine.Debug.LogError($"Failed to uncompress payload"); RunShellCommand(CommandRemoveDirectoryTree(workDir)); return false; } return PostDecompressActions(workDir); } /// /// Installs a single payload by decompressing it into the configured destination. /// private bool InstallPayload(PayloadDescriptor pd) { return DecompressSysroot(pd.path, pd.dir); } /// /// Returns a platform-appropriate command to create a directory (mkdir). /// private string CommandCreateDirectory(NPath dir) { _initStatus = InitializationStatus.Failed; #if UNITY_EDITOR_WIN return $"mkdir {dir.InQuotes(SlashMode.Native)}"; #else return $"mkdir -p {dir.InQuotes()}"; #endif } /// /// Returns the fully quoted path to the 7-Zip binary Unity ships with the Editor. /// private string Get7zPath() { #if UNITY_EDITOR_WIN string command = $"{EditorApplication.sevenZipPath}"; #else string command = EditorApplication.sevenZipPath; #endif return new NPath($"{command}").InQuotes(SlashMode.Native); } /// /// Builds a command that extracts into . /// /// /// Windows: 7z x -so | 7z x -ttar -si (7-Zip for both layers).
/// Unix: 7z x -so | tar xf - (7-Zip then system tar). ///
private string CommandUncompressTarball(NPath tarball, NPath destDir) { #if UNITY_EDITOR_WIN return $"{Get7zPath()} x -y {tarball.InQuotes(SlashMode.Native)} -so | {Get7zPath()} x -y -aoa -ttar -si"; #else return $"{Get7zPath()} x -y {tarball.InQuotes()} -so | tar xf - --directory={destDir.InQuotes()}"; #endif } /// /// Returns a platform-appropriate command to remove an entire directory tree. /// private string CommandRemoveDirectoryTree(NPath dir) { #if UNITY_EDITOR_WIN return $"rd /s /q {dir.InQuotes(SlashMode.Native)}"; #else return $"rm -rf {dir.InQuotes()}"; #endif } /// /// Hook for subclasses to perform additional fixups after extraction (e.g., symlinks, RPATH tweaks). /// /// The directory the payload was extracted to. /// to continue initialization; to fail. private bool PostDecompressActions(NPath workDir) { return true; } /// /// Returns the base folder used for per-user Unity data on the current OS. /// /// /// macOS: ~/Library/Unity
/// Windows/Linux: %LOCALAPPDATA%/unity3d ///
private string UserAppDataFolder() { return #if UNITY_EDITOR_OSX $"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}/Library/Unity"; #else $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}/unity3d"; #endif } /// /// Computes the absolute installation directory for a given payload under the sysroot cache. /// /// Relative path (e.g., linux-arm64/<version>). /// Absolute cache path where the payload should be installed. /// /// Uses the UNITY_SYSROOT_CACHE environment variable if set; otherwise defaults to /// {UserAppDataFolder}/cache/sysroots. /// internal NPath PayloadInstallDirectory(string payloadDir) { string cacheDir = Environment.GetEnvironmentVariable("UNITY_SYSROOT_CACHE"); if (string.IsNullOrEmpty(cacheDir)) cacheDir = $"{UserAppDataFolder()}/cache/sysroots"; return new NPath($"{cacheDir}/{payloadDir}"); } } }