diff --git a/.github/docker/headless/linux/Dockerfile b/.github/docker/headless/linux/Dockerfile index 68a8a5599..6df700d2e 100644 --- a/.github/docker/headless/linux/Dockerfile +++ b/.github/docker/headless/linux/Dockerfile @@ -9,7 +9,12 @@ RUN apt-get update && \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY ./HeadlessLinux64/. . -RUN chmod +x ./HeadlessLinuxServer.x86_64 +ARG ARTIFACT_DIR=HeadlessLinux64 +COPY ./${ARTIFACT_DIR}/. . +RUN set -eux; \ + exe="$(find . -maxdepth 1 -type f \( -name 'HeadlessLinuxServer.x86_64' -o -name 'HeadlessLinuxServer.arm64' -o -name 'HeadlessLinuxServer' \) | head -n1)"; \ + test -n "$exe"; \ + chmod +x "$exe"; \ + ln -sf "$(basename "$exe")" /app/HeadlessLinuxServer -ENTRYPOINT ["./HeadlessLinuxServer.x86_64"] +ENTRYPOINT ["./HeadlessLinuxServer"] diff --git a/.github/scripts/install_unity_package_latest.py b/.github/scripts/install_unity_package_latest.py new file mode 100644 index 000000000..9a555221c --- /dev/null +++ b/.github/scripts/install_unity_package_latest.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import sys +import urllib.request +from pathlib import Path + + +UNITY_REGISTRY_URL = "https://packages.unity.com" + + +def fetch_latest_version(package_name: str) -> str: + url = f"{UNITY_REGISTRY_URL}/{package_name}" + with urllib.request.urlopen(url, timeout=30) as response: + package_info = json.load(response) + + latest = package_info.get("dist-tags", {}).get("latest") + if not latest: + raise RuntimeError(f"Package {package_name} did not expose dist-tags.latest at {url}") + + return latest + + +def update_manifest_dependency(manifest_path: Path, package_name: str, version: str) -> None: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + dependencies = manifest.setdefault("dependencies", {}) + previous_version = dependencies.get(package_name) + dependencies[package_name] = version + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + if previous_version is None: + print(f"Added {package_name}@{version} to {manifest_path}") + else: + print(f"Updated {package_name}: {previous_version} -> {version} in {manifest_path}") + + +def main() -> int: + if len(sys.argv) != 3: + print("Usage: install_unity_package_latest.py ", file=sys.stderr) + return 1 + + project_root = Path(sys.argv[1]).resolve() + package_name = sys.argv[2].strip() + manifest_path = project_root / "Packages" / "manifest.json" + + if not manifest_path.exists(): + print(f"Manifest not found: {manifest_path}", file=sys.stderr) + return 1 + + latest_version = fetch_latest_version(package_name) + print(f"Resolved latest {package_name} version: {latest_version}") + update_manifest_dependency(manifest_path, package_name, latest_version) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/build-docker-server.yml b/.github/workflows/build-docker-server.yml index 9e7796a78..86e9b3176 100644 --- a/.github/workflows/build-docker-server.yml +++ b/.github/workflows/build-docker-server.yml @@ -89,7 +89,7 @@ jobs: run: 'echo "Image pushed with digest: ${{ steps.build.outputs.digest }}"' headless-build: - name: Build headless for ${{ matrix.targetPlatform }} + name: Build headless for ${{ matrix.targetPlatform }}${{ matrix.linuxArchitecture && format(' - {0}', matrix.linuxArchitecture) || '' }} timeout-minutes: 100 runs-on: ${{ matrix.buildPlatform }} permissions: @@ -103,14 +103,28 @@ jobs: - targetPlatform: StandaloneLinux64 buildPlatform: ubuntu-latest buildName: HeadlessLinuxServer - buildOutput: LinuxServer - artifactName: LinuxHeadless + buildOutput: LinuxServerAmd64 + artifactName: LinuxHeadlessAmd64 + headlessFolderName: HeadlessLinux64 + linuxArchitecture: X64 + customParameters: -standaloneBuildSubtarget Server -linuxArchitecture X64 + buildMethod: BasisHeadlessBuild.BuildLinuxServer + - targetPlatform: StandaloneLinux64 + buildPlatform: ubuntu-latest + buildName: HeadlessLinuxServer + buildOutput: LinuxServerArm64 + artifactName: LinuxHeadlessArm64 + headlessFolderName: HeadlessLinuxArm64 + linuxArchitecture: ARM64 + customParameters: -standaloneBuildSubtarget Server -linuxArchitecture ARM64 buildMethod: BasisHeadlessBuild.BuildLinuxServer - targetPlatform: StandaloneWindows64 buildPlatform: ubuntu-latest buildName: HeadlessWindowsServer buildOutput: WindowsServer artifactName: WindowsHeadless + headlessFolderName: HeadlessWindows64 + customParameters: -standaloneBuildSubtarget Server buildMethod: BasisHeadlessBuild.BuildWindowsServer needs: [check-secret] if: needs.check-secret.outputs.secret-is-set == 'true' @@ -144,6 +158,11 @@ jobs: shell: bash run: | python3 .github/scripts/sanitize_headless_ci.py "${projectPath}" + - name: "Install Linux Arm64 Unity SDK package" + if: matrix.targetPlatform == 'StandaloneLinux64' && matrix.linuxArchitecture == 'ARM64' + shell: bash + run: | + python3 .github/scripts/install_unity_package_latest.py "${projectPath}" "com.unity.sdk.linux-arm64" - name: "Disable OpenVR editor setup for Windows headless builds" if: runner.os == 'Windows' shell: pwsh @@ -178,7 +197,7 @@ jobs: with: buildName: ${{ matrix.buildName }} buildMethod: ${{ matrix.buildMethod }} - customParameters: -standaloneBuildSubtarget Server + customParameters: ${{ matrix.customParameters }} projectPath: ${{ env.projectPath }} targetPlatform: ${{ matrix.targetPlatform }} versioning: None @@ -194,15 +213,15 @@ jobs: if: matrix.targetPlatform == 'StandaloneLinux64' shell: bash run: | - sudo mv "build/${{ matrix.buildOutput }}/StandaloneLinux64" "build/${{ matrix.buildOutput }}/HeadlessLinux64" - sudo chown -R "$(id -u):$(id -g)" "build/${{ matrix.buildOutput }}/HeadlessLinux64" + sudo mv "build/${{ matrix.buildOutput }}/StandaloneLinux64" "build/${{ matrix.buildOutput }}/${{ matrix.headlessFolderName }}" + sudo chown -R "$(id -u):$(id -g)" "build/${{ matrix.buildOutput }}/${{ matrix.headlessFolderName }}" - name: "Write headless config.xml into artifact" shell: bash run: | if [ "${{ matrix.targetPlatform }}" = "StandaloneWindows64" ]; then - data_dir="build/${{ matrix.buildOutput }}/HeadlessWindows64/HeadlessWindowsServer_Data" + data_dir="build/${{ matrix.buildOutput }}/${{ matrix.headlessFolderName }}/HeadlessWindowsServer_Data" else - data_dir="build/${{ matrix.buildOutput }}/HeadlessLinux64/HeadlessLinuxServer_Data" + data_dir="build/${{ matrix.buildOutput }}/${{ matrix.headlessFolderName }}/HeadlessLinuxServer_Data" fi mkdir -p "$data_dir" @@ -239,20 +258,34 @@ jobs: path: build/${{ matrix.buildOutput }} headless-docker-linux: - name: Build & push headless Linux image + name: Build & push headless Linux image (${{ matrix.archSuffix }}) runs-on: ubuntu-latest needs: [headless-build] permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + include: + - artifactName: LinuxHeadlessAmd64 + dockerPlatform: linux/amd64 + archSuffix: amd64 + artifactDir: HeadlessLinux64 + - artifactName: LinuxHeadlessArm64 + dockerPlatform: linux/arm64 + archSuffix: arm64 + artifactDir: HeadlessLinuxArm64 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Download Linux headless artifact uses: actions/download-artifact@v4 with: - name: LinuxHeadless + name: ${{ matrix.artifactName }} path: headless + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry @@ -268,25 +301,65 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.HEADLESS_IMAGE_NAME }} tags: | - type=raw,value=nightly-linux,enable=${{ github.ref == 'refs/heads/developer' }} - type=raw,value=latest-linux,enable=${{ github.ref == 'refs/heads/long-term-support' }} - type=sha,prefix={{branch}}-,suffix=-linux,format=short - type=ref,event=branch,suffix=-linux + type=raw,value=nightly-linux-${{ matrix.archSuffix }},enable=${{ github.ref == 'refs/heads/developer' }} + type=raw,value=latest-linux-${{ matrix.archSuffix }},enable=${{ github.ref == 'refs/heads/long-term-support' }} + type=sha,prefix={{branch}}-,suffix=-linux-${{ matrix.archSuffix }},format=short + type=ref,event=branch,suffix=-linux-${{ matrix.archSuffix }} - name: Build and push headless Linux image uses: docker/build-push-action@v5 id: build with: context: ./headless file: ./.github/docker/headless/linux/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.dockerPlatform }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + ARTIFACT_DIR=${{ matrix.artifactDir }} - name: Image digest run: 'echo "Headless Linux image pushed with digest: ${{ steps.build.outputs.digest }}"' + headless-docker-linux-manifest: + name: Publish headless Linux manifest + runs-on: ubuntu-latest + needs: [headless-docker-linux] + permissions: + contents: read + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.HEADLESS_IMAGE_NAME }} + tags: | + type=raw,value=nightly-linux,enable=${{ github.ref == 'refs/heads/developer' }} + type=raw,value=latest-linux,enable=${{ github.ref == 'refs/heads/long-term-support' }} + type=sha,prefix={{branch}}-,suffix=-linux,format=short + type=ref,event=branch,suffix=-linux + - name: Create Linux multi-arch manifest + shell: bash + run: | + while IFS= read -r tag; do + [ -n "$tag" ] || continue + docker buildx imagetools create \ + --tag "$tag" \ + "${tag}-amd64" \ + "${tag}-arm64" + done <<< "${{ steps.meta.outputs.tags }}" + headless-docker-windows: name: Build & push headless Windows image runs-on: ubuntu-latest diff --git a/Basis/Packages/com.basis.bundlemanagement/BasisIOManagement.cs b/Basis/Packages/com.basis.bundlemanagement/BasisIOManagement.cs index cf6842435..386e6eb4f 100644 --- a/Basis/Packages/com.basis.bundlemanagement/BasisIOManagement.cs +++ b/Basis/Packages/com.basis.bundlemanagement/BasisIOManagement.cs @@ -152,7 +152,7 @@ public static async Task> DownloadBEEEx(string url, if (platformSectionData == null || platformSectionData.Length == 0) { - return BeeResult.Fail($"DownloadBEEEx: No platform-matching section found in connector. Platform Request was {Application.platform} using platform keys -> {BasisBundleConnector.DebugOfPlatforms()}"); + return BeeResult.Fail($"DownloadBEEEx: No platform-matching section found in connector. Platform Request was {Application.platform}. {BasisBundleConnector.DebugOfPlatforms(connector)}"); } // 5) Write local .bee (Int32 header + connector + section) diff --git a/Basis/Packages/com.basis.sdk/Scripts/BasisBundleConnector.cs b/Basis/Packages/com.basis.sdk/Scripts/BasisBundleConnector.cs index 37b2c44c0..a08bbf448 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/BasisBundleConnector.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/BasisBundleConnector.cs @@ -75,9 +75,31 @@ public static bool IsPlatform(BasisBundleGenerated platformBundle) { Enum.GetName(typeof(BuildTarget), BuildTarget.StandaloneLinux64), new HashSet { RuntimePlatform.LinuxEditor, RuntimePlatform.LinuxPlayer, RuntimePlatform.LinuxServer } }, { Enum.GetName(typeof(BuildTarget), BuildTarget.iOS), new HashSet { RuntimePlatform.IPhonePlayer } } }; - public static string DebugOfPlatforms() + public static string DebugOfPlatforms(BasisBundleConnector connector = null) { - return string.Join("\n", platformMappings.Select(kvp => $" {kvp.Key} => [{string.Join(", ", kvp.Value)}]")); + string bundlePlatforms = " "; + + if (connector != null) + { + if (connector.BasisBundleGenerated == null || connector.BasisBundleGenerated.Length == 0) + { + bundlePlatforms = " "; + } + else + { + var platforms = connector.BasisBundleGenerated + .Where(bundle => bundle != null) + .Select(bundle => string.IsNullOrWhiteSpace(bundle.Platform) ? "" : bundle.Platform) + .ToArray(); + + bundlePlatforms = platforms.Length == 0 + ? " " + : string.Join("\n", platforms.Select(platform => $" {platform}")); + } + } + + string knownPlatforms = string.Join("\n", platformMappings.Select(kvp => $" {kvp.Key} => [{string.Join(", ", kvp.Value)}]")); + return $"Bundle Generated Platforms:\n{bundlePlatforms}\nKnown Platform Mappings:\n{knownPlatforms}"; } public enum BuildTarget { diff --git a/Basis/Assets/Editor/BasisHeadlessBuild.cs b/Basis/Packages/com.basis.server/Editor/BasisHeadlessBuild.cs similarity index 90% rename from Basis/Assets/Editor/BasisHeadlessBuild.cs rename to Basis/Packages/com.basis.server/Editor/BasisHeadlessBuild.cs index ecea0d771..ada153cfe 100644 --- a/Basis/Assets/Editor/BasisHeadlessBuild.cs +++ b/Basis/Packages/com.basis.server/Editor/BasisHeadlessBuild.cs @@ -30,6 +30,7 @@ private static void BuildServer(BuildTarget target) string buildName = GetArgument("customBuildName") ?? Path.GetFileNameWithoutExtension(buildPath); string projectPath = GetArgument("projectPath") ?? Directory.GetCurrentDirectory(); string standaloneSubtargetArg = GetArgument("standaloneBuildSubtarget") ?? "Server"; + string linuxArchitectureArg = GetArgument("linuxArchitecture"); Debug.Log($"[BasisHeadlessBuild] Starting {target} build"); Debug.Log($"[BasisHeadlessBuild] projectPath={projectPath}"); @@ -38,6 +39,7 @@ private static void BuildServer(BuildTarget target) Debug.Log($"[BasisHeadlessBuild] activeBuildTarget(before)={EditorUserBuildSettings.activeBuildTarget}"); Debug.Log($"[BasisHeadlessBuild] activeBuildTargetGroup(before)={BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget)}"); Debug.Log($"[BasisHeadlessBuild] standaloneBuildSubtarget(arg)={standaloneSubtargetArg}"); + Debug.Log($"[BasisHeadlessBuild] linuxArchitecture(arg)={linuxArchitectureArg ?? ""}"); BuildTargetGroup targetGroup = BuildPipeline.GetBuildTargetGroup(target); if (!BuildPipeline.IsBuildTargetSupported(targetGroup, target)) @@ -53,6 +55,12 @@ private static void BuildServer(BuildTarget target) StandaloneBuildSubtarget standaloneSubtarget = ParseStandaloneSubtarget(standaloneSubtargetArg); EditorUserBuildSettings.standaloneBuildSubtarget = standaloneSubtarget; + if (target == BuildTarget.StandaloneLinux64) + { + int linuxArchitecture = ParseLinuxArchitecture(linuxArchitectureArg); + PlayerSettings.SetArchitecture(targetGroup, linuxArchitecture); + Debug.Log($"[BasisHeadlessBuild] Linux architecture(set)={linuxArchitecture}"); + } Debug.Log($"[BasisHeadlessBuild] activeBuildTarget(after)={EditorUserBuildSettings.activeBuildTarget}"); Debug.Log($"[BasisHeadlessBuild] standaloneBuildSubtarget(set)={EditorUserBuildSettings.standaloneBuildSubtarget}"); @@ -166,6 +174,25 @@ private static StandaloneBuildSubtarget ParseStandaloneSubtarget(string value) return StandaloneBuildSubtarget.Server; } + private static int ParseLinuxArchitecture(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return 0; + } + + switch (value.Trim().ToUpperInvariant()) + { + case "ARM64": + return 1; + case "UNIVERSAL": + return 2; + case "X64": + default: + return 0; + } + } + private static string RequireArgument(string name) { string value = GetArgument(name);