File size: 8,015 Bytes
05c9ac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import os
import shutil
import subprocess
import yaml
from sys import platform
from typing import List, Optional, Mapping


def get_unity_executable_path():
    if platform == "darwin":
        downloader_install_path = "./.Editor/Unity.app/Contents/MacOS/Unity"
    else:  # if platform == "linux":
        downloader_install_path = "./.Editor/Unity"
    if os.path.exists(downloader_install_path):
        return downloader_install_path
    raise FileNotFoundError("Can't find executable from unity-downloader-cli")


def get_base_path():
    # We might need to do some more work here if the working directory ever changes
    # E.g. take the full path and back out the main module main.
    # But for now, this should work
    return os.getcwd()


def get_base_output_path():
    """ "
    Returns the artifact folder to use for yamato jobs.
    """
    return os.path.join(get_base_path(), "artifacts")


def run_standalone_build(
    base_path: str,
    verbose: bool = False,
    output_path: str = None,
    scene_path: str = None,
    build_target: str = None,
    log_output_path: Optional[str] = f"{get_base_output_path()}/standalone_build.txt",
) -> int:
    """
    Run BuildStandalonePlayerOSX test to produce a player. The location defaults to
    artifacts/standalonebuild/testPlayer.
    """
    unity_exe = get_unity_executable_path()
    print(f"Running BuildStandalonePlayer via {unity_exe}")

    # enum values from https://docs.unity3d.com/2022.3/Documentation/ScriptReference/BuildTarget.html
    build_target_to_enum: Mapping[Optional[str], str] = {
        "mac": "StandaloneOSX",
        "osx": "StandaloneOSX",
        "linux": "StandaloneLinux64",
    }
    # Convert the short name to the official enum
    # Just pass through if it's not on the list.
    build_target_enum = build_target_to_enum.get(build_target, build_target)

    test_args = [
        unity_exe,
        "-projectPath",
        f"{base_path}/Project",
        "-batchmode",
        "-executeMethod",
        "Unity.MLAgents.StandaloneBuildTest.BuildStandalonePlayerOSX",
    ]

    if log_output_path:
        os.makedirs(os.path.dirname(log_output_path), exist_ok=True)
        subprocess.run(["touch", log_output_path])
        test_args += ["-logfile", log_output_path]
    else:
        # Log to stdout
        test_args += ["-logfile", "-"]

    if output_path is not None:
        output_path = os.path.join(get_base_output_path(), output_path)
        test_args += ["--mlagents-build-output-path", output_path]
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
    if scene_path is not None:
        test_args += ["--mlagents-build-scene-path", scene_path]
    if build_target_enum is not None:
        test_args += ["--mlagents-build-target", build_target_enum]
    print(f"{' '.join(test_args)} ...")

    timeout = 30 * 60  # 30 minutes, just in case
    res: subprocess.CompletedProcess = subprocess.run(test_args, timeout=timeout)

    # Copy the default build name into the artifacts folder.
    if output_path is None and res.returncode == 0:
        exe_name = "testPlayer.app" if platform == "darwin" else "testPlayer"
        shutil.move(
            os.path.join(base_path, "Project", exe_name),
            os.path.join(get_base_output_path(), exe_name),
        )

    # Print if we fail or want verbosity.
    if verbose or res.returncode != 0:
        if log_output_path:
            subprocess.run(["cat", log_output_path])

    return res.returncode


def find_executables(root_dir: str) -> List[str]:
    """
    Try to find the player executable. This seems to vary between Unity versions.
    """
    ignored_extension = frozenset([".dll", ".dylib", ".bundle"])
    ignored_files = frozenset(["macblas"])
    exes = []
    for root, _, files in os.walk(root_dir):
        for filename in files:
            file_root, ext = os.path.splitext(filename)
            if ext in ignored_extension or filename in ignored_files:
                continue
            file_path = os.path.join(root, filename)
            if os.access(file_path, os.X_OK):
                exes.append(file_path)
    # Also check the input path
    if os.access(root_dir, os.X_OK):
        exes.append(root_dir)
    return exes


def init_venv(
    mlagents_python_version: str = None, extra_packages: Optional[List[str]] = None
) -> None:
    """
    Install the necessary packages for the venv
    :param mlagents_python_version: The version of mlagents python packcage to install.
        If None, will do a local install, otherwise will install from pypi
    :return:
    """
    pip_commands = ["--upgrade pip", "--upgrade setuptools"]
    if mlagents_python_version:
        # install from pypi
        pip_commands += [
            f"mlagents=={mlagents_python_version}",
            # TODO build these and publish to internal pypi
            "tf2onnx==1.6.1",
        ]
    else:
        # Local install
        pip_commands += ["-e ./ml-agents-envs", "-e ./ml-agents"]
    if extra_packages:
        pip_commands += extra_packages

    for cmd in pip_commands:
        pip_index_url = "--index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple"
        print(f'Running "python3 -m pip install -q {cmd} {pip_index_url}"')
        subprocess.check_call(
            f"python3 -m pip install -q {cmd} {pip_index_url}", shell=True
        )


def checkout_csharp_version(csharp_version):
    """
    Checks out the specific git revision (usually a tag) for the C# package and Project.
    If csharp_version is None, no changes are made.
    :param csharp_version:
    :return:
    """
    if csharp_version is None:
        return

    csharp_tag = f"com.unity.ml-agents_{csharp_version}"
    csharp_dirs = ["com.unity.ml-agents", "com.unity.ml-agents.extensions", "Project"]
    for csharp_dir in csharp_dirs:
        subprocess.check_call(f"rm -rf {csharp_dir}", shell=True)
        # Allow the checkout to fail, since the extensions folder isn't availabe in 1.0.0
        subprocess.call(f"git checkout {csharp_tag} -- {csharp_dir}", shell=True)


def undo_git_checkout():
    """
    Clean up the git working directory.
    """
    subprocess.check_call("git reset HEAD .", shell=True)
    subprocess.check_call("git checkout -- .", shell=True)
    # Ensure the cache isn't polluted with old compiled assemblies.
    subprocess.check_call("rm -rf Project/Library", shell=True)


def override_config_file(src_path, dest_path, overrides):
    """
    Override settings in a trainer config file. For example,
        override_config_file(src_path, dest_path, max_steps=42)
    will copy the config file at src_path to dest_path, but override the max_steps field to 42 for all brains.
    """
    with open(src_path) as f:
        configs = yaml.safe_load(f)
        behavior_configs = configs["behaviors"]

    for config in behavior_configs.values():
        _override_config_dict(config, overrides)

    with open(dest_path, "w") as f:
        yaml.dump(configs, f)


def _override_config_dict(config, overrides):
    for key, val in overrides.items():
        if isinstance(val, dict):
            _override_config_dict(config[key], val)
        else:
            config[key] = val


def override_legacy_config_file(python_version, src_path, dest_path, **kwargs):
    """
    Override settings in a trainer config file, using an old version of the src_path. For example,
        override_config_file("0.16.0", src_path, dest_path, max_steps=42)
    will sync the file at src_path from version 0.16.0, copy it to dest_path, and override the
    max_steps field to 42 for all brains.
    """
    # Sync the old version of the file
    python_tag = f"python-packages_{python_version}"
    subprocess.check_call(f"git checkout {python_tag} -- {src_path}", shell=True)

    with open(src_path) as f:
        configs = yaml.safe_load(f)

    for config in configs.values():
        config.update(**kwargs)

    with open(dest_path, "w") as f:
        yaml.dump(configs, f)