Skip to content

Commit 3210d7a

Browse files
chore: support adaptor wheel overrides on Windows and Linux (#531)
Signed-off-by: Karthik Bekal Pattathana <133984042+karthikbekalp@users.noreply.github.com>
1 parent d2221e8 commit 3210d7a

4 files changed

Lines changed: 437 additions & 94 deletions

File tree

src/deadline/cinema4d_submitter/adaptor_override_environment.yaml

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,20 @@ environment:
1616
script:
1717
actions:
1818
onEnter:
19-
command: '{{Env.File.Enter}}'
19+
# The Cinema 4D Conda environment provides "python" on Windows and Linux.
20+
command: python
21+
args:
22+
- '{{Env.File.SetupAdaptor}}'
23+
- '{{Session.WorkingDirectory}}'
24+
- '{{Param.OverrideAdaptorWheels}}'
25+
- '{{Param.OverrideAdaptorName}}'
26+
cancelation:
27+
mode: NOTIFY_THEN_TERMINATE
2028
embeddedFiles:
21-
- name: Enter
22-
filename: override-adaptor-enter.sh
29+
- name: SetupAdaptor
30+
filename: setup-adaptor.py
2331
type: TEXT
2432
runnable: true
2533
data: |
26-
#!/bin/env bash
27-
28-
set -euo pipefail
29-
30-
echo "The adaptor wheels that are attached to the job:"
31-
ls '{{Param.OverrideAdaptorWheels}}'
32-
echo ""
33-
34-
# Create a venv and activate it in this environment
35-
echo "Creating Python venv for the {{Param.OverrideAdaptorName}} command"
36-
/usr/local/bin/python3 -m venv '{{Session.WorkingDirectory}}/venv'
37-
{{Env.File.InitialVars}}
38-
. '{{Session.WorkingDirectory}}/venv/bin/activate'
39-
{{Env.File.CaptureVars}}
40-
echo ""
41-
42-
echo "Installing adaptor into the venv"
43-
pip install '{{Param.OverrideAdaptorWheels}}'/openjd*.whl
44-
pip install '{{Param.OverrideAdaptorWheels}}'/deadline*.whl
45-
echo ""
46-
47-
if [ ! -f '{{Session.WorkingDirectory}}/venv/bin/{{Param.OverrideAdaptorName}}' ]; then
48-
echo "The Override Adaptor {{Param.OverrideAdaptorName}} was not installed as expected."
49-
exit 1
50-
fi
51-
- name: InitialVars
52-
filename: initial-vars
53-
type: TEXT
54-
runnable: true
55-
data: |
56-
#!/usr/bin/env python3
57-
import os, json
58-
envfile = "{{Session.WorkingDirectory}}/.envInitial"
59-
with open(envfile, "w", encoding="utf8") as f:
60-
json.dump(dict(os.environ), f)
61-
- name: CaptureVars
62-
filename: capture-vars
63-
type: TEXT
64-
runnable: true
65-
data: |
66-
#!/usr/bin/env python3
67-
import os, json, sys
68-
envfile = "{{Session.WorkingDirectory}}/.envInitial"
69-
if os.path.isfile(envfile):
70-
with open(envfile, "r", encoding="utf8") as f:
71-
before = json.load(f)
72-
else:
73-
print("No initial environment found, must run Env.File.CaptureVars script first")
74-
sys.exit(1)
75-
after = dict(os.environ)
76-
77-
put = {k: v for k, v in after.items() if v != before.get(k)}
78-
delete = {k for k in before if k not in after}
79-
80-
for k, v in put.items():
81-
print(f"updating {k}={v}")
82-
print(f"openjd_env: {k}={v}")
83-
for k in delete:
84-
print(f"openjd_unset_env: {k}")
34+
# Replaced with setup_adaptor_wheels.py while building the job template.
35+
pass

src/deadline/cinema4d_submitter/cinema4d_render_submitter.py

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .font_utils import FONTS_DIR, get_font_manager_environment, scene_has_fonts
4343
from .platform_utils import is_macos, is_windows
4444
from .scene import Animation, Scene, get_renderer_warning
45+
from .setup_adaptor_wheels import _find_wheels
4546
from .style import C4D_STYLE
4647
from .takes import TakeSelection
4748
from .template_timeout_patcher import add_timeouts_to_job_template
@@ -256,6 +257,54 @@ def _get_parameter_definition(
256257
return parameter
257258

258259

260+
def _get_adaptor_override_environment(wheels_path: Path) -> dict[str, Any]:
261+
if not wheels_path.is_dir():
262+
raise RuntimeError(
263+
"The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels "
264+
f"directory does not exist:\n{wheels_path}"
265+
)
266+
267+
try:
268+
_find_wheels(wheels_path)
269+
except RuntimeError as exc:
270+
raise RuntimeError(
271+
"The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels "
272+
f"directory contains the wrong wheels:\n{exc}"
273+
) from exc
274+
275+
with open(Path(__file__).parent / "adaptor_override_environment.yaml") as file:
276+
override_environment = yaml.safe_load(file)
277+
278+
override_adaptor_wheels_param = _get_parameter_definition(
279+
override_environment["parameterDefinitions"],
280+
"OverrideAdaptorWheels",
281+
)
282+
override_adaptor_wheels_param["default"] = str(wheels_path)
283+
override_adaptor_name_param = _get_parameter_definition(
284+
override_environment["parameterDefinitions"],
285+
"OverrideAdaptorName",
286+
)
287+
override_adaptor_name_param["default"] = "cinema4d-openjd"
288+
289+
setup_script_path = Path(__file__).parent / "setup_adaptor_wheels.py"
290+
setup_script = setup_script_path.read_text(encoding="utf8")
291+
setup_file = next(
292+
(
293+
embedded_file
294+
for embedded_file in override_environment["environment"]["script"]["embeddedFiles"]
295+
if embedded_file["name"] == "SetupAdaptor"
296+
),
297+
None,
298+
)
299+
if setup_file is None:
300+
raise RuntimeError(
301+
"Adaptor override environment is missing the 'SetupAdaptor' embedded file"
302+
)
303+
setup_file["data"] = setup_script
304+
305+
return override_environment
306+
307+
259308
def _get_job_template(
260309
settings: RenderSubmitterUISettings,
261310
renderers: set[str],
@@ -387,40 +436,9 @@ def _get_job_template(
387436

388437
# If this developer option is enabled, merge the adaptor_override_environment
389438
if settings.include_adaptor_wheels:
390-
with open(Path(__file__).parent / "adaptor_override_environment.yaml") as f:
391-
override_environment = yaml.safe_load(f)
392-
393439
# Read DEVELOPMENT.md for instructions to create the wheels directory.
394440
wheels_path = Path(__file__).parent.parent.parent.parent / "wheels"
395-
if not wheels_path.exists() and wheels_path.is_dir():
396-
raise RuntimeError(
397-
"The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels directory does not exist:\n"
398-
+ str(wheels_path)
399-
)
400-
wheels_path_package_names = {
401-
path.split("-", 1)[0] for path in os.listdir(wheels_path) if path.endswith(".whl")
402-
}
403-
if wheels_path_package_names != {
404-
"openjd_adaptor_runtime",
405-
"deadline",
406-
"deadline_cloud_for_cinema4d",
407-
}:
408-
raise RuntimeError(
409-
"The Developer Option 'Include Adaptor Wheels' is enabled, but the wheels directory contains the wrong wheels:\n"
410-
+ "Expected: openjd_adaptor_runtime, deadline, and deadline_cloud_for_cinema4d\n"
411-
+ f"Actual: {wheels_path_package_names}"
412-
)
413-
414-
override_adaptor_wheels_param = _get_parameter_definition(
415-
override_environment["parameterDefinitions"],
416-
"OverrideAdaptorWheels",
417-
)
418-
override_adaptor_wheels_param["default"] = str(wheels_path)
419-
override_adaptor_name_param = _get_parameter_definition(
420-
override_environment["parameterDefinitions"],
421-
"OverrideAdaptorName",
422-
)
423-
override_adaptor_name_param["default"] = "cinema4d-openjd"
441+
override_environment = _get_adaptor_override_environment(wheels_path)
424442

425443
# There are no parameter conflicts between these two templates, so this works
426444
job_template["parameterDefinitions"].extend(override_environment["parameterDefinitions"])
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
"""Set up locally built adaptor wheels on a Deadline Cloud worker."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import os
8+
import re
9+
import subprocess
10+
import sys
11+
from collections.abc import Sequence
12+
from importlib.metadata import distributions
13+
from pathlib import Path
14+
15+
_EXPECTED_WHEEL_PREFIXES = (
16+
"openjd_adaptor_runtime-",
17+
"deadline-",
18+
"deadline_cloud_for_cinema_4d-",
19+
)
20+
21+
22+
def _find_wheels(wheels_dir: Path) -> list[Path]:
23+
wheels = sorted(wheels_dir.glob("*.whl"))
24+
selected: list[Path] = []
25+
26+
for prefix in _EXPECTED_WHEEL_PREFIXES:
27+
matches = [wheel for wheel in wheels if wheel.name.startswith(prefix)]
28+
if len(matches) != 1:
29+
raise RuntimeError(
30+
f"Expected exactly one wheel matching '{prefix}*.whl' in {wheels_dir}, "
31+
f"found {[wheel.name for wheel in matches]}"
32+
)
33+
selected.append(matches[0])
34+
35+
if len(wheels) != len(selected):
36+
unexpected = sorted(set(wheels) - set(selected))
37+
raise RuntimeError(
38+
"The adaptor wheels directory contains unexpected wheels: "
39+
+ ", ".join(wheel.name for wheel in unexpected)
40+
)
41+
42+
return selected
43+
44+
45+
def _get_venv_paths(venv_dir: Path, adaptor_name: str) -> tuple[Path, Path, Path]:
46+
if os.name == "nt":
47+
bin_dir = venv_dir / "Scripts"
48+
return bin_dir, bin_dir / "python.exe", bin_dir / f"{adaptor_name}.exe"
49+
50+
bin_dir = venv_dir / "bin"
51+
return bin_dir, bin_dir / "python", bin_dir / adaptor_name
52+
53+
54+
def _get_site_packages(venv_python: Path) -> Path:
55+
result = subprocess.run(
56+
[
57+
str(venv_python),
58+
"-c",
59+
"import sysconfig; print(sysconfig.get_paths()['purelib'])",
60+
],
61+
check=True,
62+
capture_output=True,
63+
text=True,
64+
)
65+
return Path(result.stdout.strip())
66+
67+
68+
def _get_distribution_version(site_packages: Path, distribution_name: str) -> str:
69+
canonical_name = re.sub(r"[-_.]+", "-", distribution_name).lower()
70+
for distribution in distributions(path=[str(site_packages)]):
71+
installed_name = distribution.metadata["Name"]
72+
if installed_name and re.sub(r"[-_.]+", "-", installed_name).lower() == canonical_name:
73+
return distribution.version
74+
raise RuntimeError(f"Could not find '{distribution_name}' in {site_packages}")
75+
76+
77+
def _install_wheels(venv_python: Path, wheels: list[Path]) -> None:
78+
# The active Conda environment supplies transitive dependencies. Development
79+
# wheels use generated versions that may not satisfy each other's release ranges.
80+
subprocess.run(
81+
[
82+
str(venv_python),
83+
"-m",
84+
"pip",
85+
"install",
86+
"--disable-pip-version-check",
87+
"--force-reinstall",
88+
"--no-deps",
89+
*[str(wheel) for wheel in wheels],
90+
],
91+
check=True,
92+
)
93+
94+
95+
def _prioritize_site_packages(site_packages: Path) -> None:
96+
# Queue dependencies may be exposed through PYTHONPATH. Keep that environment
97+
# unchanged for Cinema 4D, but make the attached wheels win module resolution.
98+
(site_packages / "_deadline_adaptor_override.pth").write_text(
99+
f"import sys; sys.path.insert(0, {str(site_packages)!r})\n",
100+
encoding="utf8",
101+
)
102+
103+
104+
def _emit_environment_changes(
105+
before: dict[str, str],
106+
*,
107+
venv_dir: Path,
108+
venv_bin: Path,
109+
) -> None:
110+
after = dict(before)
111+
after["PATH"] = os.pathsep.join(filter(None, (str(venv_bin), before.get("PATH", ""))))
112+
after["VIRTUAL_ENV"] = str(venv_dir)
113+
after.pop("PYTHONHOME", None)
114+
115+
for key, value in sorted(after.items()):
116+
if value != before.get(key):
117+
print(f"openjd_env: {key}={value}")
118+
119+
for key in sorted(before):
120+
if key not in after:
121+
print(f"openjd_unset_env: {key}")
122+
123+
124+
def _parse_args(argv: Sequence[str] | None = None) -> tuple[Path, Path, str]:
125+
parser = argparse.ArgumentParser()
126+
parser.add_argument("working_directory")
127+
parser.add_argument("wheels_directory")
128+
parser.add_argument("adaptor_name")
129+
args = parser.parse_args(argv)
130+
return Path(args.working_directory), Path(args.wheels_directory), args.adaptor_name
131+
132+
133+
def main(argv: Sequence[str] | None = None) -> None:
134+
working_dir, wheels_dir, adaptor_name = _parse_args(argv)
135+
before_environment = dict(os.environ)
136+
137+
print(f"Setting up {adaptor_name} from attached wheels on {sys.platform}")
138+
wheels = _find_wheels(wheels_dir)
139+
for wheel in wheels:
140+
print(f" {wheel.name}")
141+
142+
venv_dir = working_dir / "adaptor-venv"
143+
print(f"Creating adaptor virtual environment at {venv_dir}")
144+
subprocess.run(
145+
[sys.executable, "-m", "venv", "--system-site-packages", str(venv_dir)],
146+
check=True,
147+
)
148+
149+
venv_bin, venv_python, adaptor_executable = _get_venv_paths(venv_dir, adaptor_name)
150+
_install_wheels(venv_python, wheels)
151+
152+
if not adaptor_executable.is_file():
153+
raise RuntimeError(
154+
f"The override adaptor '{adaptor_name}' was not installed at {adaptor_executable}"
155+
)
156+
157+
site_packages = _get_site_packages(venv_python)
158+
_prioritize_site_packages(site_packages)
159+
adaptor_version = _get_distribution_version(site_packages, "deadline-cloud-for-cinema-4d")
160+
print(
161+
"ADAPTOR_OVERRIDE_READY "
162+
f"executable={adaptor_executable} "
163+
f"package=deadline-cloud-for-cinema-4d version={adaptor_version}"
164+
)
165+
166+
_emit_environment_changes(
167+
before_environment,
168+
venv_dir=venv_dir,
169+
venv_bin=venv_bin,
170+
)
171+
172+
173+
if __name__ == "__main__":
174+
try:
175+
main()
176+
except Exception as exc:
177+
print(f"ADAPTOR_OVERRIDE_FAILED: {exc}", file=sys.stderr)
178+
raise

0 commit comments

Comments
 (0)