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