From 9767c549935bd781795fd3d26f0f5f4c5ab66a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= Date: Wed, 23 Sep 2026 14:31:17 +0200 Subject: [PATCH 1/3] Initial BuildAutomation module implementation --- .../build-automation/standalone-projects.yml | 113 ++++++ .yamato/project-builders/builder.metafile | 22 -- .yamato/project-builders/project-builders.yml | 97 ----- Tools/CI/NGO.Cookbook.csproj | 7 +- Tools/CI/Program.cs | 3 + .../CI/Settings/NgoBuildAutomationSettings.cs | 40 ++ .../scripts/BuildAutomation/BuilderScripts.cs | 164 ++++++++ .../Unity.ProjectBuild.Editor.asmdef | 0 .../extend_source_sparse_checkout.py | 65 ++++ .../BuildAutomation/get_build_method.py | 70 ++++ .../BuildAutomation/manifest_update.py | 154 ++++++++ .../BuildAutomation/package_parsing.py | 23 ++ .../BuildAutomation/print_build_summary.py | 226 +++++++++++ .../Asteroids.patterns | 2 + .../BossRoom.patterns | 2 + .../SocialHub.patterns | 2 + .../resolve_sparse_file_references.py | 165 ++++++++ .../BuildAutomation/setup_build_scripts.py | 38 ++ .../BuildAutomation/validate_params.py | 366 ++++++++++++++++++ .../scripts/BuildAutomation/BuilderScripts.cs | 181 --------- Tools/scripts/BuildAutomation/FileCopy.py | 59 --- .../BuildAutomation/connect_services.py | 46 --- .../BuildAutomation/disable-enable-burst.py | 120 ------ .../BuildAutomation/manifest_update.py | 62 --- .../BuildAutomation/validate_params.py | 58 --- 25 files changed, 1439 insertions(+), 646 deletions(-) create mode 100644 .yamato/build-automation/standalone-projects.yml delete mode 100644 .yamato/project-builders/builder.metafile delete mode 100644 .yamato/project-builders/project-builders.yml create mode 100644 Tools/CI/Settings/NgoBuildAutomationSettings.cs create mode 100644 Tools/CI/scripts/BuildAutomation/BuilderScripts.cs rename Tools/{ => CI}/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef (100%) create mode 100644 Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py create mode 100644 Tools/CI/scripts/BuildAutomation/get_build_method.py create mode 100644 Tools/CI/scripts/BuildAutomation/manifest_update.py create mode 100644 Tools/CI/scripts/BuildAutomation/package_parsing.py create mode 100644 Tools/CI/scripts/BuildAutomation/print_build_summary.py create mode 100644 Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns create mode 100644 Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns create mode 100644 Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns create mode 100644 Tools/CI/scripts/BuildAutomation/resolve_sparse_file_references.py create mode 100644 Tools/CI/scripts/BuildAutomation/setup_build_scripts.py create mode 100644 Tools/CI/scripts/BuildAutomation/validate_params.py delete mode 100644 Tools/scripts/BuildAutomation/BuilderScripts.cs delete mode 100644 Tools/scripts/BuildAutomation/FileCopy.py delete mode 100644 Tools/scripts/BuildAutomation/connect_services.py delete mode 100644 Tools/scripts/BuildAutomation/disable-enable-burst.py delete mode 100644 Tools/scripts/BuildAutomation/manifest_update.py delete mode 100644 Tools/scripts/BuildAutomation/validate_params.py diff --git a/.yamato/build-automation/standalone-projects.yml b/.yamato/build-automation/standalone-projects.yml new file mode 100644 index 0000000000..c415d40942 --- /dev/null +++ b/.yamato/build-automation/standalone-projects.yml @@ -0,0 +1,113 @@ +####################################################### +### ### +### Auto-generated by Recipe Engine, DO NOT EDIT. ### +### ### +####################################################### +# Source: RecipeEngine.Modules.BuildAutomation.Recipes.ProjectBuildersRecipe +# Generated by RecipeEngine.Modules.BuildAutomation +build_asteroids_project: + name: Asteroids + agent: + image: package-ci/win10:v4 + type: Unity::VM + flavor: b1.xlarge + commands: + - command: pip install --upgrade unity-downloader-cli + - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: git clone --single-branch --branch %SAMPLE_BRANCH% https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git C:/ClonedProject + - command: python Tools/CI/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/./Packages/manifest.json --local-tested-packages-path %YAMATO_SOURCE_DIR%/. + - command: python Tools/CI/scripts/BuildAutomation/print_build_summary.py --project-name Asteroids --build-machine-os windows --sample-repo-url https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git --project-path C:/ClonedProject/. + - command: unity-downloader-cli --unity-version %UNITY_VERSION% --path C:/TestingEditor --wait --fast -c editor -c il2cpp -c android -c macos + - command: python Tools/CI/scripts/BuildAutomation/setup_build_scripts.py --project-path C:/ClonedProject/. + - command: python Tools/CI/scripts/BuildAutomation/get_build_method.py --project-path C:/ClonedProject/. --unity-path C:/TestingEditor/Unity.exe + - command: python -c "import os, shutil; dst = './PlayerArtifacts/Asteroids'; src = r'C:/ClonedProject/./build'; os.makedirs(dst, exist_ok=True); shutil.copytree(src, dst, dirs_exist_ok=True)" + artifacts: + logs: + paths: + - artifacts/**/* + players: + paths: + - PlayerArtifacts/**/* + variables: + LOCAL_TESTED_PACKAGES_NAMES: com.unity.netcode.gameobjects + PLATFORM_(WIN64_MAC_ANDROID): win64 + SAMPLE_BRANCH: main + SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp + UNITY_VERSION: 6000.7 + source: + files: + sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns + checkout_mode: sparse +build_bossroom_project: + name: BossRoom + agent: + image: package-ci/win10:v4 + type: Unity::VM + flavor: b1.xlarge + commands: + - command: pip install --upgrade unity-downloader-cli + - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: git clone --single-branch --branch %SAMPLE_BRANCH% https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git C:/ClonedProject + - command: python Tools/CI/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/./Packages/manifest.json --local-tested-packages-path %YAMATO_SOURCE_DIR%/. + - command: python Tools/CI/scripts/BuildAutomation/print_build_summary.py --project-name BossRoom --build-machine-os windows --sample-repo-url https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git --project-path C:/ClonedProject/. + - command: unity-downloader-cli --unity-version %UNITY_VERSION% --path C:/TestingEditor --wait --fast -c editor -c il2cpp -c android -c macos + - command: python Tools/CI/scripts/BuildAutomation/setup_build_scripts.py --project-path C:/ClonedProject/. + - command: python Tools/CI/scripts/BuildAutomation/get_build_method.py --project-path C:/ClonedProject/. --unity-path C:/TestingEditor/Unity.exe + - command: python -c "import os, shutil; dst = './PlayerArtifacts/BossRoom'; src = r'C:/ClonedProject/./build'; os.makedirs(dst, exist_ok=True); shutil.copytree(src, dst, dirs_exist_ok=True)" + artifacts: + logs: + paths: + - artifacts/**/* + players: + paths: + - PlayerArtifacts/**/* + variables: + LOCAL_TESTED_PACKAGES_NAMES: com.unity.netcode.gameobjects + PLATFORM_(WIN64_MAC_ANDROID): win64 + SAMPLE_BRANCH: ngo-playtest-update + SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp + UNITY_VERSION: 6000.7 + source: + files: + sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns + checkout_mode: sparse +build_socialhub_project: + name: SocialHub + agent: + image: package-ci/win10:v4 + type: Unity::VM + flavor: b1.xlarge + commands: + - command: pip install --upgrade unity-downloader-cli + - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: git clone --filter=blob:none --no-checkout --single-branch --branch %SAMPLE_BRANCH% https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git C:/ClonedProject + - command: git -C C:/ClonedProject sparse-checkout set Basic/DistributedAuthoritySocialHub + - command: git -C C:/ClonedProject checkout %SAMPLE_BRANCH% + - command: python Tools/CI/scripts/BuildAutomation/resolve_sparse_file_references.py --cloned-root C:/ClonedProject --project-path Basic/DistributedAuthoritySocialHub --branch %SAMPLE_BRANCH% + - command: python Tools/CI/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/Basic/DistributedAuthoritySocialHub/Packages/manifest.json --local-tested-packages-path %YAMATO_SOURCE_DIR%/. + - command: python Tools/CI/scripts/BuildAutomation/print_build_summary.py --project-name DistributedAuthoritySocialHub --build-machine-os windows --sample-repo-url https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git --project-path C:/ClonedProject/Basic/DistributedAuthoritySocialHub + - command: unity-downloader-cli --unity-version %UNITY_VERSION% --path C:/TestingEditor --wait --fast -c editor -c il2cpp -c android -c macos + - command: python Tools/CI/scripts/BuildAutomation/setup_build_scripts.py --project-path C:/ClonedProject/Basic/DistributedAuthoritySocialHub + - command: python Tools/CI/scripts/BuildAutomation/get_build_method.py --project-path C:/ClonedProject/Basic/DistributedAuthoritySocialHub --unity-path C:/TestingEditor/Unity.exe + - command: python -c "import os, shutil; dst = './PlayerArtifacts/SocialHub'; src = r'C:/ClonedProject/Basic/DistributedAuthoritySocialHub/build'; os.makedirs(dst, exist_ok=True); shutil.copytree(src, dst, dirs_exist_ok=True)" + artifacts: + logs: + paths: + - artifacts/**/* + players: + paths: + - PlayerArtifacts/**/* + variables: + LOCAL_TESTED_PACKAGES_NAMES: com.unity.netcode.gameobjects + PLATFORM_(WIN64_MAC_ANDROID): win64 + SAMPLE_BRANCH: main + SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp + UNITY_VERSION: 6000.7 + source: + files: + sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns + checkout_mode: sparse + diff --git a/.yamato/project-builders/builder.metafile b/.yamato/project-builders/builder.metafile deleted file mode 100644 index b261d28dea..0000000000 --- a/.yamato/project-builders/builder.metafile +++ /dev/null @@ -1,22 +0,0 @@ -# https://internaldocs.unity.com/yamato_continuous_integration/usage/templating/ - -NetcodeProjects: - # Note that we are using internal Unity repo. This means that we may test with newest changes that are not yet released to our users (there are also public versions) - # The parameters specify repo location, default branch to use (since you can modify it via job variable), path to manifest.json file so we can replace package entry and path to the project root folder since it differs between projects - # Note that for BossRoom 'main' branch supports NGOv1.X and 'develop' branch supports NGOv2.X - # TODO: For BossRoom correct branch to develop when PR-3736 is merged in their repo - BossRoom: - GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git" - defaultBranch: ngo-playtest-update - manifestPath: Packages/manifest.json - projectPath: '.' - Asteroids: - GithubRepo: "https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git" - defaultBranch: main - manifestPath: Packages/manifest.json - projectPath: '.' - SocialHub: - GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git" - defaultBranch: main - manifestPath: Basic/DistributedAuthoritySocialHub/Packages/manifest.json - projectPath: 'Basic/DistributedAuthoritySocialHub' diff --git a/.yamato/project-builders/project-builders.yml b/.yamato/project-builders/project-builders.yml deleted file mode 100644 index 327598d9ea..0000000000 --- a/.yamato/project-builders/project-builders.yml +++ /dev/null @@ -1,97 +0,0 @@ -{% metadata_file .yamato/project-builders/builder.metafile %} ---- -# The above line corresponds to https://internaldocs.unity.com/yamato_continuous_integration/usage/templating/ - -# Yamato paths disclaimer: -# Note that artifacts can get only local paths and since cloned project is in different location, you need to be careful with paths -# All artifact paths are defined relative to the Yamato source directory (YAMATO_SOURCE_DIR) and are case-sensitive. -# !!! important “Artifact directory locations” All artifact globs are defined relative to the source directory (YAMATO_SOURCE_DIR). Yamato can’t capture artifacts from outside this directory, so if you need files from elsewhere, you should copy them into your source directory as part of job commands. - -# Those jobs were created in order to (in most cases) speed up playtesting and development time. -# The aim is to collect all possible projects that use Netcode for Entities and create a job that will build the project with the given version of N4E package. -# The package is taken directly from the branch from which the job was triggered. So image triggering the job from release/2.0.0 and release/2.1.0 branch to compare differences -# Example use case would be to trigger the build job on Sunday so by Monday morning all builds are ready for playtesting. This limits the time a dev/QA has to spend building projects for different configurations (win, mac, Android, scripting backends, burst etc) or simply time necessary for building huge projects (Megacity). - -# This job takes parameters as scriptable backend configuration, burst compilation, unity editor version and platform. -# Since Yamato variables can't be restricted to only specific values, the job will validate the parameters passed to it and fail quickly if those are incorrect. In order to see what options are available, please look at the variable name. - -# Note that for now all of those builds are being made on Windows machine (so for example combination of macOS + il2cpp is expected to fail) -# TODO: for now all builds are being made on Windows machine, but it would be nice to have a Mac build as well. -# TODO: add iOS support -{% for netcodeProject in NetcodeProjects -%} -build_{{ netcodeProject[0] }}_project: - name: {{ netcodeProject[0] }} - agent: - type: Unity::VM - image: package-ci/win10:v4 - flavor: b1.xlarge - variables: - UNITY_VERSION: trunk - SCRIPTING_BACKEND_IL2CPP_MONO: il2cpp - BURST_ON_OFF: on - PLATFORM_WIN64_MAC_ANDROID: win64 - SAMPLE_BRANCH: {{ netcodeProject[1].defaultBranch }} - commands: - # Validate inputs passed via Yamato variables - - python Tools/scripts/BuildAutomation/validate_params.py - - echo Building {{ netcodeProject[0] }} project from branch %SAMPLE_BRANCH% with Unity version of %UNITY_VERSION%, Scripting backend %SCRIPTING_BACKEND_IL2CPP_MONO%, Burst %BURST_ON_OFF% for platform %PLATFORM_WIN64_MAC_ANDROID% - - # Clone the external project repository into a specific directory. Notice that branch is also specified. - - git clone --single-branch --branch %SAMPLE_BRANCH% {{ netcodeProject[1].GithubRepo }} C:/ClonedProject - - # Modify the external project's manifest to use the local N4E package from current branch on which this Yamato job is running. (requires python that should be preinstalled in the image) - - python Tools/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/{{ netcodeProject[1].manifestPath }} --local-package-path %YAMATO_SOURCE_DIR%/com.unity.netcode.gameobjects - - # Run python script to update ProjectSettings.asset in order to connect the project to Unity Services/set proper values. - # Notice that if a project has this already set up then in theory we don't need to run this script. - - python Tools/scripts/BuildAutomation/connect_services.py --project-settings-path C:/ClonedProject/{{ netcodeProject[1].projectPath }}/ProjectSettings/ProjectSettings.asset - - # Enable or disable Burst compilation. This step is specific to Netcode package (or any package that uses Burst) - - IF "%BURST_ON_OFF%"=="on" (python Tools/scripts/BuildAutomation/disable-enable-burst.py --enable-burst --project-path C:/ClonedProject/{{ netcodeProject[1].projectPath }}) - ELSE (python Tools/scripts/BuildAutomation/disable-enable-burst.py --disable-burst --project-path C:/ClonedProject/{{ netcodeProject[1].projectPath }}) - - # Download the Unity Editor version specified in the UNITY_VERSION variable. Il2cpp component is downloaded only if the SCRIPTING_BACKEND_IL2CPP_MONO is set to "il2cpp". - # TODO: we could download components only if needed - - unity-downloader-cli --fast --wait -u %UNITY_VERSION% -p C:/TestingEditor -c Editor -c il2cpp -c Android -c macOS - - # Add BuilderScript.cs to the project so we can modify and build the project using Unity Editor. - # This is a bit tricky step, notice that we also need to include proper assembly definition in order for those scripts to compile properly. - # TODO: the asmdef file can be simplified - - python Tools/scripts/BuildAutomation/FileCopy.py "C:/ClonedProject/{{ netcodeProject[1].projectPath }}" - - # Build the project using Unity Editor. This will call the given static BuilderScripts method. - # Ideally, it would be nice to parametrize the BuilderScripts (for example to pass scripting backend as parameter) but -executeMethod only calls static methods without parameters so for now we will have multiple configurations - # Notice that for Android platform even if mono is selected, il2cpp will be used since mono is not supported for Android builds. - - IF /I "%PLATFORM_WIN64_MAC_ANDROID%"=="win64" ( - IF /I "%SCRIPTING_BACKEND_IL2CPP_MONO%"=="il2cpp" ( - C:/TestingEditor/Unity.exe -projectPath C:/ClonedProject/{{ netcodeProject[1].projectPath }} -buildTarget win64 -executeMethod BuilderScripts.BuildWinIl2cpp -batchmode -logFile ./artifacts/UnityLog.txt -automated -crash-report-folder ./artifacts/CrashArtifacts -quit - ) ELSE ( - C:/TestingEditor/Unity.exe -projectPath C:/ClonedProject/{{ netcodeProject[1].projectPath }} -buildTarget win64 -executeMethod BuilderScripts.BuildWinMono -batchmode -logFile ./artifacts/UnityLog.txt -automated -crash-report-folder ./artifacts/CrashArtifacts -quit - ) - ) - ELSE IF /I "%PLATFORM_WIN64_MAC_ANDROID%"=="mac" ( - IF /I "%SCRIPTING_BACKEND_IL2CPP_MONO%"=="il2cpp" ( - C:/TestingEditor/Unity.exe -projectPath C:/ClonedProject/{{ netcodeProject[1].projectPath }} -buildTarget osx -executeMethod BuilderScripts.BuildMacIl2cpp -batchmode -logFile ./artifacts/UnityLog.txt -automated -crash-report-folder ./artifacts/CrashArtifacts -quit - ) ELSE ( - C:/TestingEditor/Unity.exe -projectPath C:/ClonedProject/{{ netcodeProject[1].projectPath }} -buildTarget osx -executeMethod BuilderScripts.BuildMacMono -batchmode -logFile ./artifacts/UnityLog.txt -automated -crash-report-folder ./artifacts/CrashArtifacts -quit - ) - ) - ELSE IF /I "%PLATFORM_WIN64_MAC_ANDROID%"=="android" ( - C:/TestingEditor/Unity.exe -projectPath C:/ClonedProject/{{ netcodeProject[1].projectPath }} -buildTarget android -executeMethod BuilderScripts.BuildAndroidIl2cpp -batchmode -logFile ./artifacts/UnityLog.txt -automated -crash-report-folder ./artifacts/CrashArtifacts -quit - ) - - # Because of this we need to ensure that all files are copied to the source directory. - # TODO: this can be omitted if I can somehow build the project in the source directory (YAMATO_SOURCE_DIR) instead of C:/CompetitiveAction - - python -c "import os; os.makedirs('./build', exist_ok=True)" # --> Create the build directory if it doesn't exist - - python -c "import shutil; shutil.copytree('C:/ClonedProject/{{ netcodeProject[1].projectPath }}/build', './build', dirs_exist_ok=True)" # --> Copy the build directory to the source directory (YAMATO_SOURCE_DIR). Remember to copy entire directory and not only exe file - - artifacts: - logs: - paths: - - '*.log' - - '*.xml' - - artifacts/**/* - players: - paths: - - build/**/* -{% endfor -%} \ No newline at end of file diff --git a/Tools/CI/NGO.Cookbook.csproj b/Tools/CI/NGO.Cookbook.csproj index e8844c4a5c..9296598611 100644 --- a/Tools/CI/NGO.Cookbook.csproj +++ b/Tools/CI/NGO.Cookbook.csproj @@ -8,11 +8,16 @@ - + + + + + + diff --git a/Tools/CI/Program.cs b/Tools/CI/Program.cs index 176215bdff..3d5e3d2d8b 100644 --- a/Tools/CI/Program.cs +++ b/Tools/CI/Program.cs @@ -1,5 +1,6 @@ using NGO.Cookbook.Settings; using RecipeEngine; +using RecipeEngine.Modules.BuildAutomation; using RecipeEngine.Modules.Wrench.Helpers; @@ -9,12 +10,14 @@ public static class Program public static int Main(string[] args) { var settings = new NGOSettings(); + var buildAutomationSettings = new NgoBuildAutomationSettings(); // ReSharper disable once UnusedVariable var engine = EngineFactory .Create() .ScanAll() .WithWrenchModule(settings.Wrench) + .WithBuildAutomation(buildAutomationSettings.BuildAutomation) .GenerateAsync().Result; return engine; } diff --git a/Tools/CI/Settings/NgoBuildAutomationSettings.cs b/Tools/CI/Settings/NgoBuildAutomationSettings.cs new file mode 100644 index 0000000000..87dd13f4c5 --- /dev/null +++ b/Tools/CI/Settings/NgoBuildAutomationSettings.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using RecipeEngine.Modules.BuildAutomation.Models; +using RecipeEngine.Modules.BuildAutomation.Settings; + +namespace NGO.Cookbook.Settings +{ + public class NgoBuildAutomationSettings + { + public NgoBuildAutomationSettings() + { + BuildAutomation = new BuildAutomationSettings( + projects: new Dictionary + { + ["BossRoom"] = new( + GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git", + ProjectPath: ".", + DefaultSampleBranch: "ngo-playtest-update", + MinSupportedEditorBySample: "6000.7", + LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, + LocalTestedPackagesPath: "."), + ["Asteroids"] = new( + GithubRepo: "https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git", + ProjectPath: ".", + DefaultSampleBranch: "main", + MinSupportedEditorBySample: "6000.7", + LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, + LocalTestedPackagesPath: "."), + ["SocialHub"] = new( + GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git", + ProjectPath: "Basic/DistributedAuthoritySocialHub", + DefaultSampleBranch: "main", + MinSupportedEditorBySample: "6000.7", + LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, + LocalTestedPackagesPath: "."), + }); + } + + public BuildAutomationSettings BuildAutomation { get; } + } +} diff --git a/Tools/CI/scripts/BuildAutomation/BuilderScripts.cs b/Tools/CI/scripts/BuildAutomation/BuilderScripts.cs new file mode 100644 index 0000000000..646a31d06f --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/BuilderScripts.cs @@ -0,0 +1,164 @@ +/////////////////////////////////////////////////////////////////////////////// +// Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. // +// You may override this file locally, but any changes will be overwritten // +// the next time CI is regenerated. Permanent fixes must be made inside: // +// RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ // +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.Build; +using UnityEngine; +using UnityEngine.Rendering; + +/// +/// Main build script materialized into the sample project. It builds the player with ALL and ONLY the enabled +/// scenes; to build a different scene set, branch off in the sample repo and clone from that branch. +/// +public class BuilderScripts : MonoBehaviour +{ + static string[] GetEnabledScenes() + { + return EditorBuildSettings.scenes + .Where(scene => scene.enabled) + .Select(scene => scene.path) + .ToArray(); + } + + static void BuildPlayer(BuildConfig config) + { + PlayerSettings.SetScriptingBackend(config.NamedBuildTarget, config.ScriptingBackend); + + if (config.GraphicsApis != null && config.GraphicsApis.Length > 0) + { + PlayerSettings.SetUseDefaultGraphicsAPIs(config.BuildTarget, false); + PlayerSettings.SetGraphicsAPIs(config.BuildTarget, config.GraphicsApis); + } + + if (config.ApplicationId != null) + PlayerSettings.SetApplicationIdentifier(config.NamedBuildTarget, config.ApplicationId); + + if (config.Architecture.HasValue) + PlayerSettings.SetArchitecture(config.NamedBuildTarget, config.Architecture.Value); + + AssetDatabase.SaveAssets(); + + var buildOptions = new BuildPlayerOptions + { + locationPathName = config.OutputPath, + target = config.BuildTarget, + options = BuildOptions.Development, + scenes = GetEnabledScenes() + }; + + BuildPipeline.BuildPlayer(buildOptions); + } + + struct BuildConfig + { + public NamedBuildTarget NamedBuildTarget; + public ScriptingImplementation ScriptingBackend; + public BuildTarget BuildTarget; + public GraphicsDeviceType[] GraphicsApis; + public string OutputPath; + // Not 'string?': materialized into projects with nullable disabled, where that annotation is a hard CS8632 error. + public string ApplicationId; + public int? Architecture; + } + + [MenuItem("Tools/Builder/Build Development Windows Il2cpp")] + static void BuildWinIl2cpp() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = ScriptingImplementation.IL2CPP, + BuildTarget = BuildTarget.StandaloneWindows64, + GraphicsApis = new[] { GraphicsDeviceType.Vulkan, GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12 }, + OutputPath = "./build/Windows_Il2cpp/PlaytestBuild.exe" + }); + + [MenuItem("Tools/Builder/Build Development Windows Mono")] + static void BuildWinMono() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = ScriptingImplementation.Mono2x, + BuildTarget = BuildTarget.StandaloneWindows64, + GraphicsApis = new[] { GraphicsDeviceType.Vulkan, GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12 }, + OutputPath = "./build/Windows_Mono/PlaytestBuild.exe" + }); + + [MenuItem("Tools/Builder/Build Development Mac Mono")] + static void BuildMacMono() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = ScriptingImplementation.Mono2x, + BuildTarget = BuildTarget.StandaloneOSX, + GraphicsApis = new[] { GraphicsDeviceType.Metal }, + OutputPath = "./build/macOS_Mono/PlaytestBuild.app" + }); + + [MenuItem("Tools/Builder/Build Development Mac Il2cpp")] + static void BuildMacIl2cpp() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = ScriptingImplementation.IL2CPP, + BuildTarget = BuildTarget.StandaloneOSX, + GraphicsApis = new[] { GraphicsDeviceType.Metal }, + OutputPath = "./build/macOS_Il2cpp/PlaytestBuild.app" + }); + + [MenuItem("Tools/Builder/Build Development Android Il2cpp")] + static void BuildAndroidIl2cpp() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Android, + ScriptingBackend = ScriptingImplementation.IL2CPP, + BuildTarget = BuildTarget.Android, + GraphicsApis = new[] { GraphicsDeviceType.Vulkan }, + OutputPath = "./build/Android_Il2cpp_Vulkan/PlaytestBuild.apk", + ApplicationId = "com.UnityTestRunner.UnityTestRunner", + Architecture = 1 + }); + +#if UNITY_6000_6_OR_NEWER + // ScriptingImplementation.CoreCLR is marked [Obsolete] ("work in progress"), so suppress CS0618 to compile under warnings-as-errors. +#pragma warning disable CS0618 + static ScriptingImplementation CoreClrBackend => ScriptingImplementation.CoreCLR; +#pragma warning restore CS0618 + + [MenuItem("Tools/Builder/Build Development Windows CoreCLR")] + static void BuildWinCoreClr() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = CoreClrBackend, + BuildTarget = BuildTarget.StandaloneWindows64, + GraphicsApis = new[] { GraphicsDeviceType.Vulkan, GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12 }, + OutputPath = "./build/Windows_CoreClr/PlaytestBuild.exe" + }); + + [MenuItem("Tools/Builder/Build Development Mac CoreCLR")] + static void BuildMacCoreClr() => BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Standalone, + ScriptingBackend = CoreClrBackend, + BuildTarget = BuildTarget.StandaloneOSX, + GraphicsApis = new[] { GraphicsDeviceType.Metal }, + OutputPath = "./build/macOS_CoreClr/PlaytestBuild.app" + }); + + [MenuItem("Tools/Builder/Build Development Android CoreCLR")] + static void BuildAndroidCoreClr() + { + // CoreCLR on Android is 64-bit only; ARMv7 (32-bit) is unsupported, so force ARM64. + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64; + BuildPlayer(new BuildConfig + { + NamedBuildTarget = NamedBuildTarget.Android, + ScriptingBackend = CoreClrBackend, + BuildTarget = BuildTarget.Android, + GraphicsApis = new[] { GraphicsDeviceType.Vulkan }, + OutputPath = "./build/Android_CoreClr/PlaytestBuild.apk", + ApplicationId = "com.UnityTestRunner.UnityTestRunner" + }); + } +#endif +} diff --git a/Tools/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef b/Tools/CI/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef similarity index 100% rename from Tools/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef rename to Tools/CI/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef diff --git a/Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py b/Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py new file mode 100644 index 0000000000..ccdfcc6eaa --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py @@ -0,0 +1,65 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Extends the job's own (source) sparse checkout to include the local tested-package folders named at +runtime by LOCAL_TESTED_PACKAGES_NAMES. + +The generated rules file materializes a baseline set at checkout time, but the packages to test are read +from the environment here so that a package added (or changed) via the LOCAL_TESTED_PACKAGES_NAMES trigger +variable is checked out on demand rather than failing because it was absent from the pre-baked rules. Adding +a path that does not exist in the repository materializes nothing and is not an error; validate_params.py is +what reports a genuinely missing package. +""" + +import argparse +import os +import subprocess +import sys + +from package_parsing import parse_package_names + +ENV_LOCAL_TESTED_PACKAGES_NAMES = "LOCAL_TESTED_PACKAGES_NAMES" + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Add local tested-package folders to the source repo's sparse checkout." + ) + parser.add_argument("--source-dir", required=True, help="Absolute path to the job's source checkout (git repo root).") + parser.add_argument( + "--local-tested-packages-path", + required=True, + help="Repo-relative base directory containing the local packages (e.g. 'Packages').", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + package_names = parse_package_names(os.environ.get(ENV_LOCAL_TESTED_PACKAGES_NAMES, "")) + if not package_names: + print(f"[INFO] {ENV_LOCAL_TESTED_PACKAGES_NAMES} is empty; no source paths to add.") + return + + base_path = args.local_tested_packages_path.strip().replace("\\", "/").strip("/") + paths = [f"{base_path}/{name}" for name in package_names] + + result = subprocess.run( + ["git", "-C", args.source_dir, "sparse-checkout", "add", *paths], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False, + ) + if result.returncode != 0: + print(f"Error extending source sparse checkout: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + + print(f"[INFO] Added to source sparse checkout: {paths}") + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/get_build_method.py b/Tools/CI/scripts/BuildAutomation/get_build_method.py new file mode 100644 index 0000000000..5625861edd --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/get_build_method.py @@ -0,0 +1,70 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Helper script that determines the appropriate BuilderScripts method and +runs Unity with the correct parameters. +""" + +import argparse +import os +import subprocess +import sys + +BUILD_CONFIGS = { + ("win64", "il2cpp"): {"method": "BuilderScripts.BuildWinIl2cpp", "buildTarget": "win64"}, + ("win64", "mono"): {"method": "BuilderScripts.BuildWinMono", "buildTarget": "win64"}, + ("win64", "coreclr"): {"method": "BuilderScripts.BuildWinCoreClr", "buildTarget": "win64"}, + ("mac", "il2cpp"): {"method": "BuilderScripts.BuildMacIl2cpp", "buildTarget": "osx"}, + ("mac", "mono"): {"method": "BuilderScripts.BuildMacMono", "buildTarget": "osx"}, + ("mac", "coreclr"): {"method": "BuilderScripts.BuildMacCoreClr", "buildTarget": "osx"}, + ("android", "il2cpp"): {"method": "BuilderScripts.BuildAndroidIl2cpp", "buildTarget": "android"}, + ("android", "coreclr"): {"method": "BuilderScripts.BuildAndroidCoreClr", "buildTarget": "android"}, +} + + +def resolve_backend(): + return os.environ.get("SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR)", "").lower() + +def execute_unity_build(project_path, unity_path): + platform = os.environ.get("PLATFORM_(WIN64_MAC_ANDROID)", "").lower() + backend = resolve_backend() + config = BUILD_CONFIGS.get((platform, backend)) + if not config: + print(f"ERROR: Invalid combination: platform={platform}, backend={backend}", file=sys.stderr) + sys.exit(1) + + cmd = [ + unity_path, + "-projectPath", + project_path, + "-buildTarget", + config["buildTarget"], + "-executeMethod", + config["method"], + "-batchmode", + "-logFile", + "./artifacts/UnityLog.txt", + "-automated", + "-crash-report-folder", + "./artifacts/CrashArtifacts", + "-quit", + ] + result = subprocess.run(cmd, check=False) + sys.exit(result.returncode) + + +def main(): + parser = argparse.ArgumentParser(description="Execute Unity build command.") + parser.add_argument("--project-path", required=True, help="Path to Unity project.") + parser.add_argument("--unity-path", default="C:/TestingEditor/Unity.exe", help="Path to Unity executable.") + args = parser.parse_args() + execute_unity_build(args.project_path, args.unity_path) + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/manifest_update.py b/Tools/CI/scripts/BuildAutomation/manifest_update.py new file mode 100644 index 0000000000..6c712811cc --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/manifest_update.py @@ -0,0 +1,154 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Updates the cloned project's manifest.json to point tested packages at local file: sources, so the +changes on the current branch are exercised against the sample project. + +!!! IMPORTANT !!! — UPM embedded package priority: +Any package folder placed directly inside the project's Packages/ directory is treated as an +embedded package and takes the absolute highest priority in UPM dependency resolution. + +To work around this, the script scans the Packages/ directory of the cloned project and removes any +embedded package whose package.json declares a name matching a target package, before writing the +local file: reference into the manifest. +""" + +import argparse +import json +import os +import shutil +import sys + +from package_parsing import parse_package_names + +SEPARATOR = "=" * 80 +ENV_LOCAL_TESTED_PACKAGES_NAMES = "LOCAL_TESTED_PACKAGES_NAMES" + + +def parse_args(): + parser = argparse.ArgumentParser(description="Update a Unity project manifest to use local package versions.") + parser.add_argument("--manifest-path", required=True, help="Absolute path to project manifest.json file.") + parser.add_argument( + "--local-tested-packages-path", + required=True, + help="Absolute path to the local packages root directory (e.g. the source dir's Packages folder).", + ) + return parser.parse_args() + + +def remove_embedded_package(packages_dir, package_name): + """ + Scans subfolders of packages_dir and removes any folder whose package.json + declares a 'name' matching package_name. + """ + if not os.path.isdir(packages_dir): + return + + for entry in os.scandir(packages_dir): + if not entry.is_dir(): + continue + package_json_path = os.path.join(entry.path, "package.json") + if not os.path.isfile(package_json_path): + continue + try: + with open(package_json_path, "r", encoding="utf-8") as f: + name = json.load(f).get("name", "") + except Exception: + continue + + if name == package_name: + shutil.rmtree(entry.path) + print(f"[INFO] Removed embedded package '{package_name}' from '{entry.path}'") + return + + +def update_manifest(manifest_data, package_name, local_packages_root): + old_value = manifest_data.get("dependencies", {}).get(package_name, "") + local_package_path = os.path.join(local_packages_root, package_name) + local_path_normalized = local_package_path.replace(os.sep, "/") + manifest_data.setdefault("dependencies", {})[package_name] = f"file:{local_path_normalized}" + return old_value, f"file:{local_path_normalized}" + + +def print_summary(manifest_data, substitutions): + print() + print(SEPARATOR) + print("TARGET PACKAGE SUBSTITUTION") + print(SEPARATOR) + for substitution in substitutions: + print(f"Package: {substitution['package_name']}") + print(f" Old value: {substitution['old_value']}") + print(f" New value: {substitution['new_value']}") + local_version = substitution["local_version"] or "" + print(f"Verified local '{substitution['package_name']}' version is: {local_version}") + print() + print(SEPARATOR) + print("FINAL MANIFEST CONTENT") + print(SEPARATOR) + dependencies = manifest_data.get("dependencies", {}) + for dep_name, dep_value in sorted(dependencies.items()): + print(f" {dep_name}: {dep_value}") + print(SEPARATOR) + print() + + +def get_package_version(package_path): + package_json_path = os.path.join(package_path, "package.json") + try: + with open(package_json_path, "r", encoding="utf-8") as f: + return json.load(f).get("version", "N/A") + except Exception: + return None + + +def main(): + args = parse_args() + + try: + package_names = parse_package_names(os.environ.get(ENV_LOCAL_TESTED_PACKAGES_NAMES, "")) + # Clearing the variable at trigger time opts out of substitution; leave the manifest untouched (plain build). + if not package_names: + print(f"[INFO] {ENV_LOCAL_TESTED_PACKAGES_NAMES} is empty; skipping local package substitution.") + return + + with open(args.manifest_path, "r", encoding="utf-8") as manifest_file: + manifest_data = json.load(manifest_file) + + packages_dir = os.path.dirname(args.manifest_path) + + substitutions = [] + for package_name in package_names: + remove_embedded_package(packages_dir, package_name) + + old_value, new_value = update_manifest( + manifest_data, + package_name, + args.local_tested_packages_path, + ) + local_package_path = os.path.join(args.local_tested_packages_path, package_name) + local_version = get_package_version(local_package_path) + substitutions.append( + { + "package_name": package_name, + "old_value": old_value, + "new_value": new_value, + "local_version": local_version, + } + ) + + with open(args.manifest_path, "w", encoding="utf-8", newline="\n") as manifest_file: + json.dump(manifest_data, manifest_file, indent=4) + + print_summary(manifest_data, substitutions) + except Exception as error: + print(f"Error updating manifest: {error}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/package_parsing.py b/Tools/CI/scripts/BuildAutomation/package_parsing.py new file mode 100644 index 0000000000..8bd9183b9d --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/package_parsing.py @@ -0,0 +1,23 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Shared parsing for the LOCAL_TESTED_PACKAGES_NAMES variable, so every build-automation script splits it the +same way. Package names may be separated by commas, semicolons, and/or whitespace in any combination, so +values entered as "a,b", "a; b", or "a b" all yield the same list. +""" + +import re + +_SEPARATORS = re.compile(r"[,;\s]+") + + +def parse_package_names(raw_value): + """Split a LOCAL_TESTED_PACKAGES_NAMES value into package names on commas, semicolons, or whitespace.""" + if not raw_value: + return [] + return [name for name in _SEPARATORS.split(raw_value.strip()) if name] diff --git a/Tools/CI/scripts/BuildAutomation/print_build_summary.py b/Tools/CI/scripts/BuildAutomation/print_build_summary.py new file mode 100644 index 0000000000..4065b252ff --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/print_build_summary.py @@ -0,0 +1,226 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Prints a formatted build configuration summary table from Yamato environment variables. +Emitted after the sample is cloned and unity-downloader-cli is installed, but before the slow editor +download, so the configuration is visible early in the Yamato log while still being able to resolve the +editor actually used by the build. +""" + +import argparse +import os +import platform +import re +import subprocess + +from package_parsing import parse_package_names + +SEP = "=" * 72 +PLATFORM_MAC = "mac" +ENV_SCRIPTING_BACKEND = "SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR)" +ENV_PLATFORM = "PLATFORM_(WIN64_MAC_ANDROID)" +ENV_SAMPLE_BRANCH = "SAMPLE_BRANCH" +ENV_UNITY_VERSION = "UNITY_VERSION" +ENV_LOCAL_TESTED_PACKAGES_NAMES = "LOCAL_TESTED_PACKAGES_NAMES" +ENV_GIT_BRANCH = "GIT_BRANCH" + +# Friendly labels for the build agent OS the job ran on (the value passed by --build-machine-os). +BUILD_MACHINE_DISPLAY = {"windows": "Windows"} + +# Length to truncate a full Git revision to, matching the form ProjectVersion.txt records (e.g. +# "6000.5.0a5 (4928cd185deb)"). +SHORT_REVISION_LENGTH = 12 + + +def run_git(source_dir, *args, check=False): + try: + result = subprocess.run( + ["git", "-C", source_dir, *args], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=check, + ) + return result.stdout.strip() if result.stdout else "" + except Exception: + return "" + + +def get_package_branch_info(): + source_dir = os.environ.get("YAMATO_SOURCE_DIR", ".") + # Yamato checks the source out at a detached HEAD, so git can't report the branch; GIT_BRANCH carries the + # branch the job runs against (the PR source branch for PR-triggered jobs). + branch = os.environ.get(ENV_GIT_BRANCH, "").strip() + commit = run_git(source_dir, "rev-parse", "--short", "HEAD") + if branch and commit: + return f"{branch} ({commit})" + if branch: + return branch + if commit: + return f" ({commit})" + return "N/A" + + +def get_sample_branch_commit(sample_repo_url, sample_branch): + try: + result = subprocess.run( + ["git", "ls-remote", sample_repo_url, f"refs/heads/{sample_branch}"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False, + ) + if result.returncode == 0 and result.stdout.strip(): + full_sha = result.stdout.strip().split()[0] + return full_sha[:10] + except Exception: + pass + return None + + +def get_editor_version_from_downloader(unity_version): + try: + # --fast resolves branch-style versions (e.g. "trunk") against cached builds; --wait falls back to a + # pending build when none is cached, so resolution does not flake. Mirrors the download/validate calls. + result = subprocess.run( + ["unity-downloader-cli", "--fast", "--wait", "-u", unity_version, "-c", "editor", "--skip-download"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False, + ) + combined = result.stderr + result.stdout + match = re.search(r"Grabbing unity release (\S+) which is revision: (\w+)", combined) + if match: + return f"{match.group(1)} ({match.group(2)})" + version_match = re.search(r"Version:\s*(\S+)", result.stderr) + sha = result.stdout.strip() + if version_match: + return f"{version_match.group(1)}{f' ({sha[:SHORT_REVISION_LENGTH]})' if sha else ''}" + except Exception: + pass + return unity_version + + +def get_editor_version_from_project(project_path): + """Read the editor the cloned sample was authored with, preferring the version+revision form.""" + try: + version_file = os.path.join(project_path, "ProjectSettings", "ProjectVersion.txt") + with open(version_file, "r", encoding="utf-8") as f: + content = f.read() + # e.g. "m_EditorVersionWithRevision: 6000.5.0a5 (4928cd185deb)" + match = re.search(r"^m_EditorVersionWithRevision:\s*(.+?)\s*$", content, re.MULTILINE) + if match: + return match.group(1).strip() + match = re.search(r"^m_EditorVersion:\s*(\S+)\s*$", content, re.MULTILINE) + if match: + return match.group(1) + except Exception: + pass + return "N/A" + + +def get_updated_editor(): + """Resolve the editor actually used by the build, as opposed to the one the sample was authored with.""" + unity_version = os.environ.get(ENV_UNITY_VERSION) + if unity_version: + return get_editor_version_from_downloader(unity_version) + return None + + +def print_mac_on_windows_note(target_platform): + """ + A macOS player cross-compiled on a Windows agent loses its executable bit and carries a quarantine + attribute, so the .app may refuse to open on the Mac. Surface the recovery commands in the job log + when (and only when) a mac build is produced on a Windows host. + """ + if target_platform.strip().lower() != PLATFORM_MAC or platform.system() != "Windows": + return + + lines = [ + "", + SEP, + " Note for Mac users (mac build produced on a Windows agent)", + SEP, + " If you are unable to open the build, run the following on the Mac:", + " xattr -c ", + " chmod -R +x ", + SEP, + "", + ] + print("\n".join(lines)) + + +def main(): + parser = argparse.ArgumentParser(description="Print build configuration summary.") + parser.add_argument("--project-name", required=True, help="Project key used in CI.") + parser.add_argument( + "--sample-repo-url", + help="GitHub URL of sample repo, used to resolve sample branch commit.", + ) + parser.add_argument( + "--project-path", + help="Path to the cloned Unity project root, used to read the sample's editor from ProjectVersion.txt.", + ) + parser.add_argument( + "--build-machine-os", + choices=sorted(BUILD_MACHINE_DISPLAY.keys()), + help="OS of the build agent the job ran on; shown in the summary.", + ) + args = parser.parse_args() + + rows = [("Project Name", args.project_name)] + + # Editor the sample was authored with vs. the editor the build actually uses. + original_editor = get_editor_version_from_project(args.project_path) if args.project_path else None + updated_editor = get_updated_editor() + + git_package_branch = get_package_branch_info() + + # The package under test is the job's own source checkout, so report its branch. + rows.append(("Package Branch", git_package_branch)) + + # SAMPLE_BRANCH env var holds the external project branch. + sample_branch = os.environ.get(ENV_SAMPLE_BRANCH, "").strip() + if sample_branch: + commit = get_sample_branch_commit(args.sample_repo_url, sample_branch) if args.sample_repo_url else None + branch_display = f"{sample_branch} ({commit})" if commit else sample_branch + else: + branch_display = f" resolved to {git_package_branch}" + rows.append(("Sample Branch", branch_display)) + + # Lead with the editor the build uses; note the project's authored editor (ProjectVersion.txt) when known. + project_editor = original_editor if original_editor and original_editor != "N/A" else None + if updated_editor and project_editor: + editor_display = f"{updated_editor} (project's ProjectVersion.txt: {project_editor})" + else: + editor_display = updated_editor or original_editor + if editor_display: + rows.append(("Editor Version", editor_display)) + + local_tested_packages = parse_package_names(os.environ.get(ENV_LOCAL_TESTED_PACKAGES_NAMES, "")) + if local_tested_packages: + rows.append(("Local Tested Packages", ", ".join(local_tested_packages))) + + rows.append(("Scripting Backend", os.environ.get(ENV_SCRIPTING_BACKEND, "N/A"))) + + target_platform = os.environ.get(ENV_PLATFORM, "N/A") + rows.append(("Target Platform", target_platform)) + + # The agent OS the build ran on, distinct from the target platform (a Windows agent can cross-build a mac + # player). Best-effort: only shown when the recipe passed --build-machine-os. + if args.build_machine_os: + rows.append(("Build Machine", BUILD_MACHINE_DISPLAY[args.build_machine_os])) + + label_width = max(len(label) for label, _ in rows) + lines = ["", SEP, " Build Configuration Summary", SEP] + for label, value in rows: + lines.append(f" {label:<{label_width}} : {value}") + lines += [SEP, ""] + + print("\n".join(lines)) + + print_mac_on_windows_note(target_platform) + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns new file mode 100644 index 0000000000..52d79c4688 --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns @@ -0,0 +1,2 @@ +Tools/CI/scripts/BuildAutomation +./com.unity.netcode.gameobjects diff --git a/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns new file mode 100644 index 0000000000..52d79c4688 --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns @@ -0,0 +1,2 @@ +Tools/CI/scripts/BuildAutomation +./com.unity.netcode.gameobjects diff --git a/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns new file mode 100644 index 0000000000..52d79c4688 --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns @@ -0,0 +1,2 @@ +Tools/CI/scripts/BuildAutomation +./com.unity.netcode.gameobjects diff --git a/Tools/CI/scripts/BuildAutomation/resolve_sparse_file_references.py b/Tools/CI/scripts/BuildAutomation/resolve_sparse_file_references.py new file mode 100644 index 0000000000..d529aaddcf --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/resolve_sparse_file_references.py @@ -0,0 +1,165 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Extends a sparse-checked-out sample clone to include the repo-relative package folders it +references via UPM `file:` dependencies. + +A sample project is cloned with a blob-less partial clone whose working tree is restricted to + (see CloneProject). The project's /Packages/manifest.json can list +`file:` dependencies whose paths (relative to the manifest's own Packages/ directory) resolve to +folders OUTSIDE -- for example NetcodeSamples references the repo-root Packages/ +folder via "file:../../../Packages/com.unity.entities". Those folders are never materialized by the +initial sparse checkout, so UPM cannot resolve the packages and the build fails. + +This script parses the cloned manifest, resolves each `file:` reference to a repo-relative +directory, and `git sparse-checkout add`s the ones that fall outside . References that +escape the repository root, that already live inside , or a missing manifest are +skipped without failing the build. + +A single pass is sufficient -- no recursion is needed. The sample author lists every repo-local +sibling package as a `file:` reference in the PROJECT manifest (NetcodeSamples lists +entities/transport/netcode/URP there). A pulled-in package's own package.json declares its +dependencies as registry version refs, not `file:` references into the monorepo, so transitive +dependencies resolve from the package registry rather than via repo-local paths. + +This must run BEFORE manifest_update.py: that script rewrites the tested packages to +`file:%YAMATO_SOURCE_DIR%/...` refs pointing at the source checkout (not the sample repo), so the +resolver must see the sample's ORIGINAL manifest and act only on the sample repo's own internal refs. +""" + +import argparse +import json +import posixpath +import subprocess +import sys + +SEPARATOR = "=" * 80 + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Add repo-relative file: package folders to a sample clone's sparse checkout." + ) + parser.add_argument("--cloned-root", required=True, help="Absolute path to the cloned sample repo (e.g. C:/ClonedProject).") + parser.add_argument("--project-path", required=True, help="Repo-relative project path (e.g. Workspace/NetcodeSamples).") + parser.add_argument("--branch", required=True, help="Branch to re-checkout so newly included blobs are fetched.") + return parser.parse_args() + + +def resolve_reference(project_path, target): + """ + Resolves a manifest `file:` target (relative to /Packages) to a repo-relative, + forward-slash directory. Returns one of: + ("escape", combined) -- the path escapes the repository root; warn and skip. + ("inside", combined) -- the path is inside ; already checked out, skip. + ("candidate", combined) -- a repo-relative dir outside ; add to the checkout. + """ + combined = posixpath.normpath(posixpath.join(project_path + "/Packages", target)) + + if combined == ".." or combined.startswith("../"): + return "escape", combined + if combined == project_path or combined.startswith(project_path + "/"): + return "inside", combined + return "candidate", combined + + +def collect_candidates(dependencies, project_path): + candidates = [] + skipped_inside = [] + skipped_escape = [] + seen = set() + + for ref in dependencies.values(): + if not str(ref).startswith("file:"): + continue + + target = str(ref)[len("file:"):] + kind, combined = resolve_reference(project_path, target) + if kind == "escape": + skipped_escape.append(combined) + elif kind == "inside": + skipped_inside.append(combined) + elif combined not in seen: + seen.add(combined) + candidates.append(combined) + + return candidates, skipped_inside, skipped_escape + + +def print_summary(candidates, skipped_inside, skipped_escape): + print() + print(SEPARATOR) + print("SPARSE CHECKOUT FILE: REFERENCE RESOLUTION") + print(SEPARATOR) + print(f"Added (outside project): {candidates if candidates else ''}") + print(f"Skipped (already in project): {skipped_inside if skipped_inside else ''}") + print(f"Skipped (escapes repo root): {skipped_escape if skipped_escape else ''}") + print(SEPARATOR) + print() + + +def main(): + args = parse_args() + + project_path = args.project_path.replace("\\", "/").strip("/") + if not project_path or project_path == ".": + print("[INFO] Project path is the repository root; no sparse checkout to extend.") + return + + manifest_path = posixpath.join(args.cloned_root.replace("\\", "/"), project_path, "Packages/manifest.json") + + try: + with open(manifest_path, "r", encoding="utf-8") as manifest_file: + manifest_data = json.load(manifest_file) + except FileNotFoundError: + print(f"[INFO] No manifest at '{manifest_path}'; nothing to resolve.") + return + except Exception as error: + print(f"Error reading manifest '{manifest_path}': {error}", file=sys.stderr) + sys.exit(1) + + dependencies = manifest_data.get("dependencies", {}) + candidates, skipped_inside, skipped_escape = collect_candidates(dependencies, project_path) + + for escaped in skipped_escape: + print(f"[WARN] file: reference resolves outside the repository root and was skipped: '{escaped}'", file=sys.stderr) + + if not candidates: + print("[INFO] No external file: references to add to the sparse checkout.") + print_summary(candidates, skipped_inside, skipped_escape) + return + + add_result = subprocess.run( + ["git", "-C", args.cloned_root, "sparse-checkout", "add", *candidates], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if add_result.returncode != 0: + print(f"Error adding paths to sparse checkout: {add_result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + + # The blob:none partial clone fetches the newly included blobs lazily on checkout, mirroring + # CloneProject's final checkout (robust across git versions). + checkout_result = subprocess.run( + ["git", "-C", args.cloned_root, "checkout", args.branch], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if checkout_result.returncode != 0: + print(f"Error checking out '{args.branch}' after extending sparse checkout: {checkout_result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + + print_summary(candidates, skipped_inside, skipped_escape) + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/setup_build_scripts.py b/Tools/CI/scripts/BuildAutomation/setup_build_scripts.py new file mode 100644 index 0000000000..6926e1341d --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/setup_build_scripts.py @@ -0,0 +1,38 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Helper script that copies build scripts into the cloned Unity project to execute build action from within +""" + +import argparse +import os +import shutil + + +def parse_args(): + parser = argparse.ArgumentParser(description="Set up build scripts in cloned project.") + parser.add_argument("--project-path", required=True, help="Path to the cloned project root.") + return parser.parse_args() + + +def main(): + args = parse_args() + target_dir = os.path.join(args.project_path, "Assets", "CI", "Scripts", "Editor") + # BuilderScripts.cs and the asmdef are materialized into this script's own directory, so resolve them + # relative to __file__. This stays correct regardless of the consumer's configured ScriptsRelativeDir. + source_dir = os.path.dirname(os.path.abspath(__file__)) + os.makedirs(target_dir, exist_ok=True) + + for filename in ["Unity.ProjectBuild.Editor.asmdef", "BuilderScripts.cs"]: + source_path = os.path.join(source_dir, filename) + target_path = os.path.join(target_dir, filename) + shutil.copy(source_path, target_path) + + +if __name__ == "__main__": + main() diff --git a/Tools/CI/scripts/BuildAutomation/validate_params.py b/Tools/CI/scripts/BuildAutomation/validate_params.py new file mode 100644 index 0000000000..1d6c03ae38 --- /dev/null +++ b/Tools/CI/scripts/BuildAutomation/validate_params.py @@ -0,0 +1,366 @@ +############################################################################### +### Auto-generated by RecipeEngine.Modules.BuildAutomation. DO NOT EDIT. ### +### You may override this file locally, but any changes will be overwritten ### +### the next time CI is regenerated. Permanent fixes must be made inside: ### +### RecipeEngine.Modules.BuildAutomation/EmbeddedScripts/ ### +############################################################################### + +""" +Validates Yamato environment variables based on predefined rules. +Checks always-required values and optional repo/feature-specific values. +Exits with non-zero status if validation fails, halting the build process early. +""" + +import argparse +import os +import re +import subprocess +import sys + +from package_parsing import parse_package_names + +ENV_SCRIPTING_BACKEND = "SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR)" +ENV_PLATFORM = "PLATFORM_(WIN64_MAC_ANDROID)" +ENV_SAMPLE_BRANCH = "SAMPLE_BRANCH" +ENV_UNITY_VERSION = "UNITY_VERSION" +ENV_LOCAL_TESTED_PACKAGES_NAMES = "LOCAL_TESTED_PACKAGES_NAMES" + +ALLOWED_SCRIPTING_BACKENDS = {"il2cpp", "mono", "coreclr"} +ALLOWED_PLATFORMS = {"win64", "mac", "android"} + +CORECLR_BACKEND = "coreclr" +# coreclr is only a supported scripting backend from this editor onward; older editors must use il2cpp/mono. +MIN_CORECLR_EDITOR_VERSION = (6000, 6) +MIN_CORECLR_EDITOR_VERSION_DISPLAY = "6000.6" + +BUILD_MACHINE_WINDOWS = "windows" +ALLOWED_BUILD_MACHINE_OS = {BUILD_MACHINE_WINDOWS} + +# Platform/scripting-backend combinations that are invalid regardless of the build machine OS. +INVALID_COMBINATIONS = [ + ( + "android", + "mono", + "Android platform with mono is not supported. " + "Mobile builds require the il2cpp or coreclr scripting backend.", + ), +] + +# Combinations that depend on which OS the build agent runs. il2cpp produces a native player, so a Windows +# IL2CPP player needs a Windows agent; a mac il2cpp player cannot be produced on the Windows agent (macOS +# agents are a planned follow-up). +INVALID_COMBINATIONS_BY_BUILD_MACHINE = { + BUILD_MACHINE_WINDOWS: [ + ( + "mac", + "il2cpp", + "Mac il2cpp players cannot be built on a Windows agent. " + "Use mono to cross-build mac on Windows.", + ), + ], +} + + +def validate_required_enum(var_name, allowed_values): + value = os.environ.get(var_name, "").strip().lower() + if not value: + return [f"ERROR: Missing required environment variable: {var_name}"] + + if value not in allowed_values: + return [ + f"ERROR: Invalid {var_name}: '{value}'. " + f"Allowed values: {sorted(list(allowed_values))}" + ] + + return [] + + +def validate_core_variables(): + errors = [] + errors.extend(validate_required_enum(ENV_PLATFORM, ALLOWED_PLATFORMS)) + errors.extend(validate_required_enum(ENV_SCRIPTING_BACKEND, ALLOWED_SCRIPTING_BACKENDS)) + return errors + + +def parse_major_minor(version): + """Parses the leading MAJOR.MINOR of a Unity version string (e.g. '6000.6.0a7' -> (6000, 6)). + Returns None when the two leading numeric components cannot be determined.""" + parts = version.split(".") + if len(parts) < 2: + return None + + numbers = [] + for part in parts[:2]: + match = re.match(r"\d+", part.strip()) + if not match: + return None + numbers.append(int(match.group())) + return tuple(numbers) + + +def parse_version_tuple(version): + """Extracts the leading numeric MAJOR[.MINOR[.PATCH]] of a Unity version string as a tuple, padding + missing trailing components with 0 (e.g. '6000.5.0a5' -> (6000, 5, 0), '6000.6' -> (6000, 6, 0)). + Returns None when no leading major number is present.""" + numbers = [] + for part in str(version).strip().split(".")[:3]: + match = re.match(r"\d+", part.strip()) + if not match: + break + numbers.append(int(match.group())) + + if not numbers: + return None + + while len(numbers) < 3: + numbers.append(0) + return tuple(numbers) + + +def parse_resolved_release(downloader_output): + """Parses the editor version unity-downloader-cli reports it resolved to, from its combined output + (e.g. 'Grabbing unity release 6000.5.0a5 which is revision: ...'). Returns None when not present.""" + match = re.search(r"Grabbing unity release (\S+)", downloader_output) + return match.group(1) if match else None + + +def validate_min_supported_editor(unity_version, downloader_output, min_supported_editor): + """Fails when the editor unity-downloader-cli actually resolved is older than the sample's minimum + supported editor. Best-effort: if either version can't be parsed, warn and pass (the floor is advisory).""" + resolved = parse_resolved_release(downloader_output) + if not resolved: + print( + f"WARNING: could not determine the resolved editor version for {ENV_UNITY_VERSION} " + f"'{unity_version}'; skipping the minimum supported editor check.", + file=sys.stderr, + ) + return [] + + resolved_parsed = parse_version_tuple(resolved) + min_parsed = parse_version_tuple(min_supported_editor) + if resolved_parsed is None or min_parsed is None: + print( + f"WARNING: could not compare resolved editor '{resolved}' against minimum supported " + f"'{min_supported_editor}'; skipping the minimum supported editor check.", + file=sys.stderr, + ) + return [] + + if resolved_parsed < min_parsed: + return [ + f"ERROR: resolved editor '{resolved}' (from {ENV_UNITY_VERSION} '{unity_version}') is older than " + f"the sample's minimum supported editor '{min_supported_editor}'." + ] + return [] + + +def coreclr_editor_error(editor_major_minor, editor_display): + """Returns an error when the coreclr backend is selected but the given editor MAJOR.MINOR is older than the + minimum. `editor_major_minor` is a (major, minor) tuple, or None to skip (undeterminable version).""" + scripting_backend = os.environ.get(ENV_SCRIPTING_BACKEND, "").strip().lower() + if scripting_backend != CORECLR_BACKEND or editor_major_minor is None: + return [] + + if editor_major_minor < MIN_CORECLR_EDITOR_VERSION: + return [ + f"ERROR: coreclr scripting backend requires editor " + f"{MIN_CORECLR_EDITOR_VERSION_DISPLAY} or newer, but the editor is '{editor_display}'." + ] + return [] + + +def validate_coreclr_editor_version(): + """coreclr is only supported from editor 6000.6 onward. Checked here against the *requested* UNITY_VERSION + when it is a concrete MAJOR.MINOR; branch-style values (e.g. "trunk", "6000.6/staging") can't be judged from + the env text, so they are deferred to the resolved-editor check in validate_unity_version. Skipped when + UNITY_VERSION is absent.""" + scripting_backend = os.environ.get(ENV_SCRIPTING_BACKEND, "").strip().lower() + if scripting_backend != CORECLR_BACKEND: + return [] + + unity_version = os.environ.get(ENV_UNITY_VERSION, "").strip() + if not unity_version: + return [] + + parsed = parse_major_minor(unity_version) + if parsed is None: + # Branch-style version: the resolved-editor coreclr check in validate_unity_version handles it. + return [] + + return coreclr_editor_error(parsed, unity_version) + + +def validate_combinations(build_machine_os): + errors = [] + platform = os.environ.get(ENV_PLATFORM, "").strip().lower() + scripting_backend = os.environ.get(ENV_SCRIPTING_BACKEND, "").strip().lower() + + combinations = INVALID_COMBINATIONS + INVALID_COMBINATIONS_BY_BUILD_MACHINE.get(build_machine_os, []) + for invalid_platform, invalid_backend, message in combinations: + if platform == invalid_platform and scripting_backend == invalid_backend: + errors.append(f"ERROR: Invalid Configuration: {message}") + return errors + + +def validate_sample_branch(github_repo_url): + errors = [] + sample_branch = os.environ.get(ENV_SAMPLE_BRANCH, "").strip() + if not sample_branch: + return [f"ERROR: Missing required environment variable: {ENV_SAMPLE_BRANCH}"] + + result = subprocess.run( + ["git", "ls-remote", "--heads", "--exit-code", github_repo_url, sample_branch], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if result.returncode != 0: + errors.append( + f"ERROR: Branch '{sample_branch}' was not found in repository '{github_repo_url}'." + ) + return errors + + +def validate_unity_version(min_supported_editor=None): + errors = [] + unity_version = os.environ.get(ENV_UNITY_VERSION, "").strip() + if not unity_version: + return [f"ERROR: Missing required environment variable: {ENV_UNITY_VERSION}"] + + try: + # --fast resolves branch-style versions (e.g. "trunk", "6000.6/staging") against already-cached builds; + # --wait is required alongside it because --fast alone exits non-zero when nothing is cached yet (trunk + # builds expire), which would spuriously reject a valid branch. Mirrors the download invocation + # (--fast --wait). --skip-download still resolves and prints the revision without fetching the editor. + result = subprocess.run( + ["unity-downloader-cli", "--fast", "--wait", "-u", unity_version, "-c", "editor", "--skip-download"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + except FileNotFoundError: + return [ + "ERROR: unity-downloader-cli is not installed; " + f"cannot validate {ENV_UNITY_VERSION}. Install it before running this check." + ] + if result.returncode != 0: + errors.append( + f"ERROR: unity-downloader-cli does not recognize {ENV_UNITY_VERSION} '{unity_version}'." + ) + return errors + + resolved_revision = result.stdout.strip() + if not resolved_revision: + errors.append( + "ERROR: unity-downloader-cli did not return a resolved revision for " + f"{ENV_UNITY_VERSION} '{unity_version}'." + ) + + combined_output = result.stderr + result.stdout + + # coreclr floor against the RESOLVED editor, so branch-style versions (e.g. "trunk", "6000.6/staging") that + # the raw env text can't reveal are still gated once the downloader resolves them. + resolved_release = parse_resolved_release(combined_output) + if resolved_release: + errors.extend(coreclr_editor_error(parse_major_minor(resolved_release), resolved_release)) + + if min_supported_editor: + errors.extend( + validate_min_supported_editor(unity_version, combined_output, min_supported_editor) + ) + return errors + + +def validate_local_tested_packages(base_path): + package_names = parse_package_names(os.environ.get(ENV_LOCAL_TESTED_PACKAGES_NAMES, "")) + errors = [] + + # Clearing the variable at trigger time opts out of substitution (plain build); nothing to validate. + if not package_names: + return [] + + for package_name in package_names: + package_dir = os.path.join(base_path, package_name) + package_json = os.path.join(package_dir, "package.json") + + if not os.path.isdir(package_dir): + errors.append( + f"ERROR: Local tested package directory does not exist: '{package_dir}'" + ) + continue + + if not os.path.isfile(package_json): + errors.append( + f"ERROR: package.json was not found for local tested package '{package_name}' " + f"at '{package_json}'" + ) + + if errors: + errors.append( + f"HINT: {ENV_LOCAL_TESTED_PACKAGES_NAMES} was parsed as {package_names}. Separate package names " + "with a comma, semicolon, or space (e.g. 'com.unity.netcode,com.unity.entities'); each must be a " + f"folder under '{base_path}' that contains a package.json." + ) + + return errors + + +def parse_args(): + parser = argparse.ArgumentParser(description="Validate Yamato variables for build automation.") + parser.add_argument( + "--check-sample-branch", + dest="sample_branch_repo_url", + help="Validate that SAMPLE_BRANCH exists in this repository URL.", + ) + parser.add_argument( + "--check-unity-version", + action="store_true", + help="Validate UNITY_VERSION by resolving it through unity-downloader-cli.", + ) + parser.add_argument( + "--check-local-tested-packages", + dest="local_tested_packages_path", + help="Validate LOCAL_TESTED_PACKAGES_NAMES under this base package path.", + ) + parser.add_argument( + "--min-supported-editor", + dest="min_supported_editor", + help="Sample's minimum supported editor; the editor resolved for UNITY_VERSION must be at least " + "this version (checked only with --check-unity-version).", + ) + parser.add_argument( + "--build-machine-os", + choices=sorted(ALLOWED_BUILD_MACHINE_OS), + default=BUILD_MACHINE_WINDOWS, + help="OS of the build agent; gates platform/scripting-backend combinations (e.g. mac+il2cpp).", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + errors = [] + errors.extend(validate_core_variables()) + errors.extend(validate_combinations(args.build_machine_os)) + errors.extend(validate_coreclr_editor_version()) + + if args.sample_branch_repo_url: + errors.extend(validate_sample_branch(args.sample_branch_repo_url)) + + if args.check_unity_version: + errors.extend(validate_unity_version(args.min_supported_editor)) + + if args.local_tested_packages_path: + errors.extend(validate_local_tested_packages(args.local_tested_packages_path)) + + if errors: + for error in errors: + print(error, file=sys.stderr) + print("\nOne or more parameters failed validation. Halting build.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/Tools/scripts/BuildAutomation/BuilderScripts.cs b/Tools/scripts/BuildAutomation/BuilderScripts.cs deleted file mode 100644 index 159dd619e9..0000000000 --- a/Tools/scripts/BuildAutomation/BuilderScripts.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Collections.Generic; -using UnityEditor; -using UnityEditor.Build; -using UnityEngine; -using System; -using System.Collections; -using UnityEngine.Rendering; -using UnityEngine.SceneManagement; - -// This script is used to automate the build process for different platforms. -// When included in the project, it can be triggered from the script the game for teh given configuration. -// Note that it's possible to have those as a button in the editor (see https://github.cds.internal.unity3d.com/unity/Megacity-Metro/blob/c3b1b16ff1f04f96fbfbcc3267696679ad4e8396/Megacity-Metro/Assets/Scripts/Utils/Editor/BuilderScript.cs) -// Ideally we would like to pass scripting backend and platform as parameters instead of having different script per each combintation but the nature of calling this nmethod via script (via -executeMethod) is that it needs to be static and the parameters are not passed in a way that we can use them. -// TODO: add iOS support -public class BuilderScripts : MonoBehaviour -{ - [MenuItem("Tools/Builder/Build Development Windows Il2cpp")] - static void BuildWinIl2cpp() - { - // This part modifies Player Settings. We only use it (for our case) to modify scripting backend - PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, ScriptingImplementation.IL2CPP); - PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows64, false); // disable auto graphic to use our own custom list - PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, new []{GraphicsDeviceType.Vulkan, GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12}); // We need to specify the graphics API for Android builds to ensure proper shader compilation. Vulkan is recommended for modern devices. - - // Below you can see additional settings that you can apply to the build: - //PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, new []{GraphicsDeviceType.Direct3D12}); - //PlayerSettings.SetArchitecture(NamedBuildTarget.Standalone,0); - - // The settings that we applied above need to be saved. - AssetDatabase.SaveAssets(); - - // We want to build all scenes in build settings, so we collect them here. - // If you want to build only specific scenes, then you could just use something like buildPlayerOptions.scenes = new[] { "Assets/Scenes/Menu.unity","Assets/Scenes/Main.unity" }; below. - List scenesToAdd = new List(); - foreach (var scene in EditorBuildSettings.scenes) - { - if (scene.enabled) - { - Debug.Log("Adding scene to build: " + scene.path); - scenesToAdd.Add(scene.path); - } - } - - // This is an equivalent of BuildPlayerOptions in the Unity Editor. - // We want to choose development build, what platform are we targetting, where to save the build and which scenes to include. - // Some of those options can be omitted when triggering this script from withing GUI since more implicit context is provided (targetGroup, subtarget) - // subtarget = (int)StandaloneBuildSubtarget.Player - // targetGroup = BuildTargetGroup.Standalone - // extraScriptingDefines = new[] { "NETCODE_DEBUG", "UNITY_CLIENT" }, - var buildPlayerOptions = new BuildPlayerOptions - { - locationPathName = "./build/Windows_Il2cpp/PlaytestBuild.exe", - target = BuildTarget.StandaloneWindows64, - options = BuildOptions.Development, - scenes = scenesToAdd.ToArray() - }; - - BuildPipeline.BuildPlayer(buildPlayerOptions); - } - - [MenuItem("Tools/Builder/Build Development Windows Mono")] - static void BuildWinMono() - { - PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, ScriptingImplementation.Mono2x); - PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows64, false); // disable auto graphic to use our own custom list - PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, new []{GraphicsDeviceType.Vulkan, GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12}); // We need to specify the graphics API for Android builds to ensure proper shader compilation. Vulkan is recommended for modern devices. - - AssetDatabase.SaveAssets(); - - List scenesToAdd = new List(); - foreach (var scene in EditorBuildSettings.scenes) - { - if (scene.enabled) - { - Debug.Log("Adding scene to build: " + scene.path); - scenesToAdd.Add(scene.path); - } - } - - var buildPlayerOptions = new BuildPlayerOptions - { - locationPathName = "./build/Windows_Mono/PlaytestBuild.exe", - target = BuildTarget.StandaloneWindows64, - options = BuildOptions.Development, - scenes = scenesToAdd.ToArray() - }; - - BuildPipeline.BuildPlayer(buildPlayerOptions); - } - - [MenuItem("Tools/Builder/Build Development Mac Mono")] - static void BuildMacMono() - { - PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, ScriptingImplementation.Mono2x); - PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneOSX, false); // disable auto graphic to use our own custom list - PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneOSX, new []{GraphicsDeviceType.Metal}); // enforcing Metal Graphics API. Without this there will be shader errors in the final build. - - AssetDatabase.SaveAssets(); - - List scenesToAdd = new List(); - foreach (var scene in EditorBuildSettings.scenes) - { - if (scene.enabled) - { - Debug.Log("Adding scene to build: " + scene.path); - scenesToAdd.Add(scene.path); - } - } - - var buildPlayerOptions = new BuildPlayerOptions - { - locationPathName = "./build/macOS_Mono/PlaytestBuild.app", - target = BuildTarget.StandaloneOSX, - options = BuildOptions.Development, - scenes = scenesToAdd.ToArray() - }; - - BuildPipeline.BuildPlayer(buildPlayerOptions); - } - - [MenuItem("Tools/Builder/Build Development Mac Il2cpp")] - static void BuildMacIl2cpp() - { - PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, ScriptingImplementation.IL2CPP); - PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneOSX, new []{GraphicsDeviceType.Metal}); // enforcing Metal Graphics API. Without this there will be shader errors in the final build. - - AssetDatabase.SaveAssets(); - - List scenesToAdd = new List(); - foreach (var scene in EditorBuildSettings.scenes) - { - if (scene.enabled) - { - Debug.Log("Adding scene to build: " + scene.path); - scenesToAdd.Add(scene.path); - } - } - - var buildPlayerOptions = new BuildPlayerOptions - { - locationPathName = "./build/macOS_Il2cpp/PlaytestBuild.app", - target = BuildTarget.StandaloneOSX, - options = BuildOptions.Development, - scenes = scenesToAdd.ToArray() - }; - - BuildPipeline.BuildPlayer(buildPlayerOptions); - } - - [MenuItem("Tools/Builder/Build Development Android Il2cpp")] - static void BuildAndroidIl2cpp() - { - PlayerSettings.SetScriptingBackend(NamedBuildTarget.Android, ScriptingImplementation.IL2CPP); - PlayerSettings.SetApplicationIdentifier(NamedBuildTarget.Android, "com.UnityTestRunner.UnityTestRunner"); // This is needed only for mobiles since by default the application identifier quite often contains invalid characters like spaces so we wan't to make sure that this has a valid value. It's needed only for mobile since that's an app store requirement - PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.Android, false); // disable auto graphic to use our own custom list - PlayerSettings.SetGraphicsAPIs(BuildTarget.Android, new []{GraphicsDeviceType.Vulkan}); // We need to specify the graphics API for Android builds to ensure proper shader compilation. Vulkan is recommended for modern devices. - PlayerSettings.SetArchitecture(BuildTargetGroup.Android,1); // An integer value associated with the architecture of the build target. 0 - None, 1 - ARM64, 2 - Universal. most modern Android devices use the ARM64 architecture - - AssetDatabase.SaveAssets(); - - List scenesToAdd = new List(); - foreach (var scene in EditorBuildSettings.scenes) - { - if (scene.enabled) - { - Debug.Log("Adding scene to build: " + scene.path); - scenesToAdd.Add(scene.path); - } - } - - var buildPlayerOptions = new BuildPlayerOptions - { - locationPathName = "./build/Android_Il2cpp_Vulkan/PlaytestBuild.apk", - target = BuildTarget.Android, - options = BuildOptions.Development, - scenes = scenesToAdd.ToArray() - }; - - BuildPipeline.BuildPlayer(buildPlayerOptions); - } -} diff --git a/Tools/scripts/BuildAutomation/FileCopy.py b/Tools/scripts/BuildAutomation/FileCopy.py deleted file mode 100644 index 5204fbb6b8..0000000000 --- a/Tools/scripts/BuildAutomation/FileCopy.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import shutil -import sys - -def main(): - """ - Cleans and prepares the 'Assets/Scripts/Editor' directory for build automation scripts. - It deletes the directory if it exists, recreates it, and copies in the necessary - assembly definition and C# script files. - """ - # --- 1. Argument Validation --- - if len(sys.argv) < 2: - print("Error: Missing required argument.") - print("Usage: python prepare_build_scripts.py ") - sys.exit(1) - - project_root = sys.argv[1] - - # --- 2. Define File Paths --- - # The target directory inside the Unity project - target_dir = os.path.join(project_root, 'Assets', 'Scripts', 'Editor') - - # The source files for build automation - source_asmdef = 'Tools/scripts/BuildAutomation/Unity.ProjectBuild.Editor.asmdef' - source_script = 'Tools/scripts/BuildAutomation/BuilderScripts.cs' - - print(f"Preparing build scripts for project at: {project_root}") - print(f"Target editor script directory: {target_dir}") - - # --- 3. Clean and Recreate Directory --- - try: - if os.path.exists(target_dir): - print(f"Directory '{target_dir}' exists. Removing it.") - shutil.rmtree(target_dir) - - print(f"Creating directory: {target_dir}") - os.makedirs(target_dir) - - except OSError as e: - print(f"Error managing directory: {e}") - sys.exit(1) - - # --- 4. Copy Build Automation Files --- - try: - print(f"Copying '{source_asmdef}' to '{target_dir}'") - shutil.copy(source_asmdef, target_dir) - - print(f"Copying '{source_script}' to '{target_dir}'") - shutil.copy(source_script, target_dir) - - except IOError as e: - print(f"Error copying files: {e}") - sys.exit(1) - - print("\nSuccessfully prepared build automation scripts.") - -if __name__ == "__main__": - main() - diff --git a/Tools/scripts/BuildAutomation/connect_services.py b/Tools/scripts/BuildAutomation/connect_services.py deleted file mode 100644 index db11eeff13..0000000000 --- a/Tools/scripts/BuildAutomation/connect_services.py +++ /dev/null @@ -1,46 +0,0 @@ -# This script modifies ProjectSettings.asset in order to connect the project to Services (like Relay). This is needed for Netcode related builds and can be probably skipped in other cases -# As a Netcode team we usually use (during Playtesting) the following settings - -# Note that cloudProjectId, projectName and organizationId are defined as secrets - -# Notice that those parameters are included as defaults but in your yml file you can override them with your own values. - -import sys -import re -import argparse -import os - -def parse_args(): - global args - parser = argparse.ArgumentParser(description='Update ProjectSettings.asset in order to properly connect to services.') - parser.add_argument('--project-settings-path', required=True, help='The absolute path to the project ProjectSettings.asset file that contains all of the services settings.') - parser.add_argument('--cloud-project-ID', default=os.getenv("CLOUDPROJECTID"), help='ID of a cloud project to which we want to connect to.') - parser.add_argument('--organization-ID', default=os.getenv("ORGANIZATIONID"), help="ID of the organization to which the cloud project belongs.") - parser.add_argument('--project-name', default=os.getenv("PROJECTNAME"), help='Name of the project to which we want to connect to.') - args = parser.parse_args() - -def main(): - """ - Modifies ProjectSettings.asset in order to connect the project to Services - """ - parse_args() - - with open(args.project_settings_path, 'r') as f: - content = f.read() - - # Use regex to replace the values. This is safer than simple string replacement. - content = re.sub(r"cloudProjectId:.*", f"cloudProjectId: {args.cloud_project_ID}", content) - content = re.sub(r"organizationId:.*", f"organizationId: {args.organization_ID}", content) - content = re.sub(r"projectName:.*", f"projectName: {args.project_name}", content) - # Ensure the project is marked as connected - content = re.sub(r"cloudEnabled:.*", "cloudEnabled: 1", content) - - with open(args.project_settings_path, 'w', encoding='UTF-8', newline='\n') as f: - f.write(content) - - print(f"[Linker] Successfully updated {args.project_settings_path} with Project ID: {args.cloud_project_ID}, Org ID: {args.organization_ID}, Project Name: {args.project_name}") - -if __name__ == "__main__": - main() - - diff --git a/Tools/scripts/BuildAutomation/disable-enable-burst.py b/Tools/scripts/BuildAutomation/disable-enable-burst.py deleted file mode 100644 index d9cd0b29e6..0000000000 --- a/Tools/scripts/BuildAutomation/disable-enable-burst.py +++ /dev/null @@ -1,120 +0,0 @@ -# An example usage would be "- python Tools/CI/Netcode/BuildAutomations/disable-enable-burst.py --project-path {{ project.path }} --platform WebGL" -# This file aims to modify BurstAotSettings file which should be present under ProjectSettings folder. -# Note that this requires Burst package to be installed as well as you need to specify the platform for which you are building since there are different settings for each. (this is taken from environment variable) -# This script is not overriding existing settings file but completely replacing it - -import argparse -import json -import os - -# Function that parses arguments of the script -def parse_args(): - global args - parser = argparse.ArgumentParser(description="Enable or disable Burst compilation and specify Unity project details.") - - # Add the mutually exclusive group for --disable-burst and --enable-burst - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--disable-burst', action='store_true', help='Disable Burst compilation.') - group.add_argument('--enable-burst', action='store_true', help='Enable Burst compilation.') - - # Add additional arguments - parser.add_argument('--project-path', required=True, help='Specify the location of the Unity project.') - - args = parser.parse_args() - - -# This function creates a new burst settings file with default values. Notice that this should almost always not be used since assumption is that in our case we have projects with Burst preinstalled -# For the "default" values I used values from NetcodeSamples project in DOTS-monorepo -def create_config(settings_path): - config_name = os.path.join(settings_path, 'BurstAotSettings_{}.json'.format(resolve_target())) - monobehaviour = { - 'Version': 4, - 'EnableBurstCompilation': True, - 'EnableOptimisations': True, - 'EnableSafetyChecks': False, - 'EnableDebugInAllBuilds': False, - 'CpuMinTargetX32': 0, - 'CpuMaxTargetX32': 0, - 'CpuMinTargetX64': 0, - 'CpuMaxTargetX64': 0, - 'CpuTargetsX32': 6, - 'CpuTargetsX64': 72, - 'OptimizeFor': 0 - } - - data = {'MonoBehaviour': monobehaviour} - with open(config_name, 'w', encoding='UTF-8', newline='\n') as f: - json.dump(data, f) - return config_name - - -# Burst has specific files for each platform, so we need to resolve the target platform to get the correct settings file. -# Note that this jobs uses environment variables to pass parameters to the script. -def resolve_target(): - # Get the platform value from the environment variable - platform_key = os.environ.get('PLATFORM_WIN64_MAC_ANDROID').lower() - - resolved_target = platform_key - if 'win64' == platform_key: - resolved_target = 'StandaloneWindows' - elif 'mac' == platform_key: - resolved_target = 'StandaloneOSX' - elif 'android' == platform_key: - resolved_target = 'Android' - else: - raise ValueError("Unsupported platform: {}".format(platform) + "Check if you are passing correct argument for one of the supported platforms: StandaloneWindows or StandaloneLinux") - - return resolved_target - - -# This function either returns existing burst settings or creates new if a file was not found -def get_or_create_burst_AOT_config(): - settings_path = os.path.join(args.project_path, 'ProjectSettings') - if not os.path.isdir(settings_path): - os.mkdir(settings_path) - config_names = [os.path.join(settings_path, filename) for filename in os.listdir(settings_path) if filename.startswith("BurstAotSettings_{}".format(resolve_target()))] - if not config_names: - return [create_config(settings_path)] - return config_names - - -# Function that sets the AOT status in the burst settings file (essentially enables or disables burst compilation) -def set_burst_AOT(config_file, status): - config = None - with open(config_file, 'r') as f: - config = json.load(f) - - assert config is not None, 'AOT settings not found; did the burst-enabled build finish successfully?' - - config['MonoBehaviour']['EnableBurstCompilation'] = status - with open(config_file, 'w', encoding='UTF-8', newline='\n') as f: - json.dump(config, f) - - -def main(): - parse_args() - config_names = get_or_create_burst_AOT_config() - - platform_key = os.environ.get('PLATFORM_WIN64_MAC_ANDROID').lower() - print(f"Burst compilation script: Unity project path is {args.project_path}") - print(f"Burst compilation script: Target platform is {platform_key}") - - if args.disable_burst: - print('BURST COMPILATION: DISABLED') - - for config_name in config_names: - set_burst_AOT(config_name, False) - - elif args.enable_burst: - print('BURST COMPILATION: ENABLED') - - for config_name in config_names: - set_burst_AOT(config_name, True) - - else: - sys.exit('BURST COMPILATION: unexpected value: {}'.format(args.enable_burst)) - - - -if __name__ == '__main__': - main() diff --git a/Tools/scripts/BuildAutomation/manifest_update.py b/Tools/scripts/BuildAutomation/manifest_update.py deleted file mode 100644 index 1597e2bc39..0000000000 --- a/Tools/scripts/BuildAutomation/manifest_update.py +++ /dev/null @@ -1,62 +0,0 @@ -# This script is used to replace a package from the project manifest.json with a local version. -# The goal is that you will trigger the build process from your branch (for example release/1.2.3) and package from this branch will be use in the project manifest.1 -# Note that for now this script is assuming that such package already has an entry in the manifest -# TODO: consider if it makes sense to just add new manifest entry (to test what?) - -import json -import argparse -import os - -def parse_args(): - global args - parser = argparse.ArgumentParser(description='Update a Unity project manifest to point to the local package version.') - parser.add_argument('--manifest-path', required=True, help='The absolute path to the project manifest.json file.') - parser.add_argument('--package-name', default="com.unity.netcode.gameobjects", help="The name of the package to modify in the manifest.") - parser.add_argument('--local-package-path', required=True, help='The absolute file path to the local package source directory.') - args = parser.parse_args() - -def main(): - """ - Updates a project's manifest.json to use package under passed local-package-path, - and then prints the version of that local package. - """ - parse_args() - - # Update the target project's manifest - try: - with open(args.manifest_path, 'r') as f: - manifest_data = json.load(f) - - local_path_normalized = args.local_package_path.replace(os.sep, '/') - manifest_data["dependencies"][args.package_name] = f"file:{local_path_normalized}" - - with open(args.manifest_path, 'w', encoding='UTF-8', newline='\n') as f: - json.dump(manifest_data, f, indent=4) - - print(f"Successfully updated manifest at '{args.manifest_path}'") - print(f"Set '{args.package_name}' to use local package at '{args.local_package_path}'") - except Exception as e: - print(f"Error updating manifest: {e}") - exit(1) - - # --- Read and report the local package's version for log confirmation--- - # This is only for debug purposes - try: - # Construct the path to the local package's package.json file - local_package_json_path = os.path.join(args.local_package_path, 'package.json') - - with open(local_package_json_path, 'r') as f: - local_package_data = json.load(f) - - # Extract the version, providing a default if not found - local_package_version = local_package_data.get('version', 'N/A') - - print(f"--> Verified local '{args.package_name}' version is: {local_package_version}") - - except FileNotFoundError: - print(f"Warning: Could not find package.json at '{local_package_json_path}'") - except Exception as e: - print(f"Error reading local package version: {e}") - -if __name__ == "__main__": - main() diff --git a/Tools/scripts/BuildAutomation/validate_params.py b/Tools/scripts/BuildAutomation/validate_params.py deleted file mode 100644 index 4c105836cc..0000000000 --- a/Tools/scripts/BuildAutomation/validate_params.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -import sys - -# --- Configuration --- -# A dictionary that maps each environment variable to a set of its allowed values. -# This is the single source of truth for all validation. -VALIDATION_RULES = { - 'SCRIPTING_BACKEND_IL2CPP_MONO': {'il2cpp', 'mono'}, - 'BURST_ON_OFF': {'on', 'off'}, - 'PLATFORM_WIN64_MAC_ANDROID': {'win64', 'mac', 'android'} -} - -def main(): - """ - Validates Yamato environment variables using a rule-based dictionary. - Exits with 1 if any variable is invalid, otherwise exits with 0. - """ - all_params_valid = True - - for var_name, allowed_values in VALIDATION_RULES.items(): - actual_value = os.environ.get(var_name, '').lower() - allowed_values_lower = {v.lower() for v in allowed_values} - - if actual_value not in allowed_values_lower: - print( - f"ERROR: Invalid {var_name}: '{actual_value}'. " - f"Allowed values are: {list(allowed_values)}", - file=sys.stderr - ) - all_params_valid = False - - platform = os.environ.get('PLATFORM_WIN64_MAC_ANDROID', '').lower() - scripting_backend = os.environ.get('SCRIPTING_BACKEND_IL2CPP_MONO', '').lower() - - if platform == 'mac' and scripting_backend == 'il2cpp': - print( - "ERROR: Invalid Configuration: The 'mac' platform with the 'il2cpp' " - "Note that for now windows machine is used for building project and it's a known limitation that mac builds (via windows machine) can be done only with mono", - file=sys.stderr - ) - all_params_valid = False - - if platform == 'android' and scripting_backend == 'mono': - print( - "ERROR: Invalid Configuration: The 'android' platform with the 'mono' " - "Note that mobile builds are not supporting mono and need il2cpp scripting backend", - file=sys.stderr - ) - all_params_valid = False - - if not all_params_valid: - print("\nOne or more parameters failed validation. Halting build.", file=sys.stderr) - sys.exit(1) - - print("All parameters are valid. Proceeding with the build.") - -if __name__ == "__main__": - main() \ No newline at end of file From f79986b508c479841e131989ed80f1b4fb776247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= Date: Thu, 24 Sep 2026 12:54:48 +0200 Subject: [PATCH 2/3] Renamed Wrench settings for beter readibility --- Tools/CI/Program.cs | 2 +- Tools/CI/Settings/{NGOSettings.cs => NgoWrenchSettings.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename Tools/CI/Settings/{NGOSettings.cs => NgoWrenchSettings.cs} (93%) diff --git a/Tools/CI/Program.cs b/Tools/CI/Program.cs index 3d5e3d2d8b..913e04d496 100644 --- a/Tools/CI/Program.cs +++ b/Tools/CI/Program.cs @@ -9,7 +9,7 @@ public static class Program { public static int Main(string[] args) { - var settings = new NGOSettings(); + var settings = new NgoWrenchSettings(); var buildAutomationSettings = new NgoBuildAutomationSettings(); // ReSharper disable once UnusedVariable diff --git a/Tools/CI/Settings/NGOSettings.cs b/Tools/CI/Settings/NgoWrenchSettings.cs similarity index 93% rename from Tools/CI/Settings/NGOSettings.cs rename to Tools/CI/Settings/NgoWrenchSettings.cs index aab47ee96b..28751e115c 100644 --- a/Tools/CI/Settings/NGOSettings.cs +++ b/Tools/CI/Settings/NgoWrenchSettings.cs @@ -5,7 +5,7 @@ namespace NGO.Cookbook.Settings; -public class NGOSettings : AnnotatedSettingsBase +public class NgoWrenchSettings : AnnotatedSettingsBase { // Path from the root of the repository where packages are located. readonly string[] packagesRootPaths = {"."}; @@ -29,7 +29,7 @@ public class NGOSettings : AnnotatedSettingsBase } }; - public NGOSettings() + public NgoWrenchSettings() { Wrench = new WrenchSettings(packagesRootPaths, PackageOptions); Wrench.PvpProfilesToCheck = new HashSet() { "supported" }; From f491498dcd35fe82e2328b01ca4a299d94ac2d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= Date: Thu, 24 Sep 2026 12:59:45 +0200 Subject: [PATCH 3/3] Updated MinSupportedEditorBySample values --- .yamato/build-automation/standalone-projects.yml | 12 ++++++------ Tools/CI/Settings/NgoBuildAutomationSettings.cs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.yamato/build-automation/standalone-projects.yml b/.yamato/build-automation/standalone-projects.yml index c415d40942..2d06d4f69b 100644 --- a/.yamato/build-automation/standalone-projects.yml +++ b/.yamato/build-automation/standalone-projects.yml @@ -14,7 +14,7 @@ build_asteroids_project: commands: - command: pip install --upgrade unity-downloader-cli - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . - - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.3 - command: git clone --single-branch --branch %SAMPLE_BRANCH% https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git C:/ClonedProject - command: python Tools/CI/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/./Packages/manifest.json --local-tested-packages-path %YAMATO_SOURCE_DIR%/. - command: python Tools/CI/scripts/BuildAutomation/print_build_summary.py --project-name Asteroids --build-machine-os windows --sample-repo-url https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git --project-path C:/ClonedProject/. @@ -34,7 +34,7 @@ build_asteroids_project: PLATFORM_(WIN64_MAC_ANDROID): win64 SAMPLE_BRANCH: main SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp - UNITY_VERSION: 6000.7 + UNITY_VERSION: 6000.3 source: files: sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/Asteroids.patterns @@ -48,7 +48,7 @@ build_bossroom_project: commands: - command: pip install --upgrade unity-downloader-cli - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . - - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.3 - command: git clone --single-branch --branch %SAMPLE_BRANCH% https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git C:/ClonedProject - command: python Tools/CI/scripts/BuildAutomation/manifest_update.py --manifest-path C:/ClonedProject/./Packages/manifest.json --local-tested-packages-path %YAMATO_SOURCE_DIR%/. - command: python Tools/CI/scripts/BuildAutomation/print_build_summary.py --project-name BossRoom --build-machine-os windows --sample-repo-url https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git --project-path C:/ClonedProject/. @@ -68,7 +68,7 @@ build_bossroom_project: PLATFORM_(WIN64_MAC_ANDROID): win64 SAMPLE_BRANCH: ngo-playtest-update SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp - UNITY_VERSION: 6000.7 + UNITY_VERSION: 6000.3 source: files: sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/BossRoom.patterns @@ -82,7 +82,7 @@ build_socialhub_project: commands: - command: pip install --upgrade unity-downloader-cli - command: python Tools/CI/scripts/BuildAutomation/extend_source_sparse_checkout.py --source-dir %YAMATO_SOURCE_DIR% --local-tested-packages-path . - - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.7 + - command: python Tools/CI/scripts/BuildAutomation/validate_params.py --build-machine-os windows --check-sample-branch https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git --check-unity-version --check-local-tested-packages %YAMATO_SOURCE_DIR%/. --min-supported-editor 6000.0 - command: git clone --filter=blob:none --no-checkout --single-branch --branch %SAMPLE_BRANCH% https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git C:/ClonedProject - command: git -C C:/ClonedProject sparse-checkout set Basic/DistributedAuthoritySocialHub - command: git -C C:/ClonedProject checkout %SAMPLE_BRANCH% @@ -105,7 +105,7 @@ build_socialhub_project: PLATFORM_(WIN64_MAC_ANDROID): win64 SAMPLE_BRANCH: main SCRIPTING_BACKEND_(IL2CPP_MONO_CORECLR): il2cpp - UNITY_VERSION: 6000.7 + UNITY_VERSION: 6000.0 source: files: sparse_checkout_rules: Tools/CI/scripts/BuildAutomation/project-sparse-checkouts/SocialHub.patterns diff --git a/Tools/CI/Settings/NgoBuildAutomationSettings.cs b/Tools/CI/Settings/NgoBuildAutomationSettings.cs index 87dd13f4c5..a81474cdcd 100644 --- a/Tools/CI/Settings/NgoBuildAutomationSettings.cs +++ b/Tools/CI/Settings/NgoBuildAutomationSettings.cs @@ -15,21 +15,21 @@ public NgoBuildAutomationSettings() GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git", ProjectPath: ".", DefaultSampleBranch: "ngo-playtest-update", - MinSupportedEditorBySample: "6000.7", + MinSupportedEditorBySample: "6000.3", LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, LocalTestedPackagesPath: "."), ["Asteroids"] = new( GithubRepo: "https://github.cds.internal.unity3d.com/unity/Asteroids-CMB-NGO-Sample.git", ProjectPath: ".", DefaultSampleBranch: "main", - MinSupportedEditorBySample: "6000.7", + MinSupportedEditorBySample: "6000.3", LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, LocalTestedPackagesPath: "."), ["SocialHub"] = new( GithubRepo: "https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize.git", ProjectPath: "Basic/DistributedAuthoritySocialHub", DefaultSampleBranch: "main", - MinSupportedEditorBySample: "6000.7", + MinSupportedEditorBySample: "6000.0", LocalTestedPackagesNames: new[] { "com.unity.netcode.gameobjects" }, LocalTestedPackagesPath: "."), });