diff --git a/queue_environments/README.md b/queue_environments/README.md index c45f8f97..b3a88e13 100644 --- a/queue_environments/README.md +++ b/queue_environments/README.md @@ -4,7 +4,7 @@ Queue environments follow the [Open Job Description environment template specifi ## Sample index -This table covers every queue environment YAML file in `queue_environments/`. +This table covers every immediate user-selectable queue environment or collection in `queue_environments/`. Nested collections provide their own complete indexes. | Sample | What it demonstrates | Start here when | |---|---|---| @@ -14,6 +14,7 @@ This table covers every queue environment YAML file in `queue_environments/`. | [Cached Conda environment](conda_queue_env_improved_caching.yaml) | Reusing hash-named environments with service-managed fleet commands | Repeated package sets should avoid relinking on every job | | [Cached inline Conda environment](conda_queue_env_inline_improved_caching.yaml) | Portable named-environment reuse and expiration logic | Customer-managed fleets need reusable Conda environments | | [Rez environment](rez_queue_env.yaml) | Resolving packages from a shared Rez repository | Your studio already distributes software with Rez | +| [Rez shim environment](rez_shim/) | Wrapping each task in a resolved Rez context through `PATH` shims | Rez software needs shell functions, aliases, or ordered `PATH` edits | | [Pip environment](pip_queue_env.yaml) | Creating a Python `venv` and installing job-selected pip packages | Jobs need Python packages without Conda or Rez | | [Disconnect UBL](disconnect_ubl_queue_env.yaml) | Removing Deadline Cloud Usage Based License environment variables | A queue must use only a custom license server | @@ -128,6 +129,12 @@ The cached inline sample implements the same idea with Conda environments identi The Rez sample resolves software from a shared package repository. Use it with customer-managed fleets that can access that repository. +### Rez shim environment + +Choose the [Rez shim environment](rez_shim/) if your Rez packages configure software with anything other than plain environment variables, such as an `alias` for a launcher, a shell function, or a `PATH` prepend that must shadow a system binary. Those cannot cross out of a queue environment as `openjd_env` name-value pairs, so the sample above loses them. The shim environment instead wraps each task's command in the resolved context. + +It comes with test scaffolding and a verification job, so it lives in its own directory with a [dedicated README](rez_shim/README.md) covering deployment, tradeoffs, and the upstream RFC that will supersede it. + ### Pip environment The pip sample uses Python's standard-library `venv` module to install `PipPackages` and activate the environment for subsequent steps. If `PipPackages` is empty it does nothing, allowing mixed queues. `PipIndexUrl` and `PipExtraIndexUrls` support private indexes such as [AWS CodeArtifact](https://docs.aws.amazon.com/codeartifact/). diff --git a/queue_environments/rez_shim/README.md b/queue_environments/rez_shim/README.md new file mode 100644 index 00000000..cba1e402 --- /dev/null +++ b/queue_environments/rez_shim/README.md @@ -0,0 +1,169 @@ +# Rez shim queue environment + +Applies a resolved Rez context to each task by wrapping its command, instead of copying environment variables out of the queue environment and replaying them. + +Choose this if your Rez packages configure software with anything other than plain environment variables. Studios commonly hit this when a package defines an `alias` for a launcher, relies on a shell function, or prepends to `PATH` expecting its own binary to shadow a system one. If your packages only set variables, the simpler [rez_queue_env.yaml](../rez_queue_env.yaml) works and needs no extra pieces. + +## Contents + +| File | Purpose | +|---|---| +| [`rez_queue_env_shim.yaml`](rez_queue_env_shim.yaml) | The queue environment to deploy on a farm | +| [`rez_demo_setup_queue_env.yaml`](rez_demo_setup_queue_env.yaml) | Test scaffolding: installs Rez and builds a demo package so the shim environment can run on a worker that has neither | +| [`demo_job_bundle/template.yaml`](demo_job_bundle/template.yaml) | A job that calls a Rez tool by bare name and verifies environment fidelity | + +Only the first file belongs on a production queue. The other two exist to demonstrate and test it. + +## Why the simpler sample cannot cover these cases + +The limitation is structural. A queue environment action runs in its own subprocess, so the only way it can affect later actions is by printing `openjd_env: NAME=value` directives. To work within that, [rez_queue_env.yaml](../rez_queue_env.yaml) activates a context and then replays the difference between the environment before and after. Anything that is not a name-value pair does not survive that round trip. + +A Rez `alias` is the clearest casualty. Rez implements it as an exported shell function, which Bash exports under a name like `BASH_FUNC_launch%%` with a multi-line value. The session runtime rejects that assignment outright: + +```text +openjd_env: "BASH_FUNC_demoalias%%=() { demorender --via-alias \"$@\"\n}" + -- ERROR: Failed to parse environment variable assignment. +``` + +The alias is gone before any task runs. + +## How the shim environment works + +`onEnter` resolves the requested packages once and saves the context to a `.rxt` file in the session directory. It then asks Rez which executables those packages provide and writes one small shim per tool, prepending the shim directory to `PATH`: + +```bash +#!/usr/bin/env bash +exec rez env --input "$REZ_CONTEXT_FILE" --shell bash -- "/abs/path/to/tool" "$@" +``` + +Job templates keep calling tools by bare name, such as `command: mayapy`, so each call re-enters the saved context in its own shell and Rez applies the full context inside the task's own process. Job bundles need no changes. + +Tool names come from `rez context -t` on the saved context, so no list of executables is hard-coded. Set `RezExtraTools` for commands a package provides without declaring them in its `tools` list. + +Each tool is resolved to an absolute path with `command -v` inside the context when its shim is written, rather than being re-resolved by name at task time. The reason is a recursion risk. By default Rez rebuilds `PATH` from the context and drops the shim directory, so a bare name is safe. On a farm whose Rez config lists `PATH` in `parent_variables`, though, the shim directory stays ahead of the package's own `bin`, and a bare name would find the shim again and fork until the worker ran out of processes. Resolving once up front removes that risk whatever the Rez configuration. + +A tool that resolves back into the shim directory, or that the context cannot resolve at all, gets no shim. The environment reports it at startup and tasks fall back to whatever the worker provides. + +## Parameters + +The shim environment defines these: + +| Parameter | Default | Purpose | +|---|---|---| +| `RezPackages` | `""` | Space-separated packages to resolve. Empty skips the environment | +| `RezRepositories` | `REZ_REPOSITORY_PATH` | Colon-separated package search path. Edit the per-platform defaults in the script for your farm | +| `RezExtraTools` | `""` | Extra command names to shim, for tools a package does not declare | + +The demo setup environment and job add these: + +| Parameter | Default | Purpose | +|---|---|---| +| `RezDemoRepository` | `/tmp/rez-demo-repository` | Where to build the demo package. Pass the same value as `RezRepositories` | +| `ToolName` | `demorender` | The command the first demo step invokes by bare name | +| `CancelSleepSeconds` | `600` | How long `CancelThroughShim` sleeps, giving you time to cancel the job | + +## Deploy on a farm + +Edit `LINUX_REZ_REPOSITORY_PATH` and `MACOS_REZ_REPOSITORY_PATH` in `rez_queue_env_shim.yaml` to your repository location, then create the queue environment: + +```console +aws deadline create-queue-environment \ + --farm-id FARM_ID \ + --queue-id QUEUE_ID \ + --priority 2 \ + --template-type YAML \ + --template file://queue_environments/rez_shim/rez_queue_env_shim.yaml +``` + +Give it a higher priority number than any other environment that edits `PATH`, such as a Conda environment, because the last writer wins. + +Workers need Rez installed and read access to the package repository. Neither is provided by service-managed fleet images by default. + +## Try it without a farm + +The demo setup environment installs Rez and builds a `demotool` package into the session, so the shim environment runs unmodified against it. Apply both environments in order: + +```console +openjd run queue_environments/rez_shim/demo_job_bundle/template.yaml \ + --environment queue_environments/rez_shim/rez_demo_setup_queue_env.yaml \ + --environment queue_environments/rez_shim/rez_queue_env_shim.yaml \ + -p RezDemoRepository=/tmp/rez-demo-repository \ + -p RezPackages=demotool \ + -p RezRepositories=/tmp/rez-demo-repository \ + --step VerifyEnvironment +``` + +Pass the same directory as the setup environment's `RezDemoRepository` and the shim environment's `RezRepositories`. + +To run it on a queue, attach the setup environment at a lower priority number than the shim environment and submit with the same parameters: + +```console +deadline bundle submit queue_environments/rez_shim/demo_job_bundle \ + -p RezDemoRepository=/tmp/rez-demo-repository \ + -p RezPackages=demotool \ + -p RezRepositories=/tmp/rez-demo-repository +``` + +The demo needs a fleet of Linux or macOS workers with `python3` and network access to PyPI. A production farm provides Rez on the worker image and does not need the setup environment at all. + +## What the demo verifies + +`RunRezTool` calls `demorender` by bare name, so the shim is what runs. `VerifyEnvironment` then runs three checks and fails the task if any regress: + +| Check | State under test | Under harvest-and-replay | +|---|---|---| +| 1 | A plain variable, `DEMOTOOL_VERSION` | Survives | +| 2 | A Rez `alias`, which becomes an exported shell function | Lost, rejected by the runtime | +| 3 | A `PATH` prepend where the package provides its own `sort` | Depends on environment order rather than the resolved context | + +Every check reads its result from a tool called by bare name, so each one depends on the shim mechanism end to end rather than on the saved context alone. Deleting the `PATH` injection from the environment fails all three, which is how the checks were confirmed to test what they claim. + +A third step, `CancelThroughShim`, is a manual check rather than an automatic one. It sleeps inside a shimmed tool for `CancelSleepSeconds` so you can cancel the job and watch the signal arrive. The tool reports the signal it caught before exiting. Cancel it from the monitor or with: + +```console +aws deadline update-job --farm-id FARM_ID --queue-id QUEUE_ID \ + --job-id JOB_ID --target-task-run-status CANCELED +``` + +Expect the step to end as `CANCELED` with `demosleep: caught SIGTERM, exiting` in the session log. Left alone it simply runs to completion. + +A successful session log shows the variable absent from the session but present inside the tool, then all three checks passing: + +```text +=== DEMOTOOL_VERSION as seen by the session (expected UNSET) === +DEMOTOOL_VERSION=UNSET +=== DEMOTOOL_VERSION as seen inside the tool (expected 1.0.0) === +demorender: DEMOTOOL_VERSION=1.0.0 +=== Check 1: plain variable reaches the tool === +PASS: variable visible inside the tool +=== Check 2: Rez alias survives into the task === +PASS: alias is callable +=== Check 3: package PATH prepend shadows the system command === +PASS: package command shadows the system one +All 3 environment fidelity checks passed. +``` + +Running the same bundle under [rez_queue_env.yaml](../rez_queue_env.yaml) instead shows the runtime refusing the alias, which is the failure this environment avoids. + +## Tradeoffs + +* Only bare command names are intercepted. A template invoking an absolute path bypasses the shims. +* Linux and macOS workers only. The shims are POSIX shell scripts that depend on a shebang line, which does not work on Windows, so the environment fails immediately there with a message pointing at the alternative. Use [rez_queue_env.yaml](../rez_queue_env.yaml) for Windows fleets. +* Each task pays a context re-entry. Rez's resolve cache keeps this small, but it is not free. + +Cancelation does reach through a shim. Rez runs the tool in a shell of its own, so the process tree is `shim` → `rez env` → shell → tool rather than flat, but a `SIGTERM` sent to the top process propagates to the tool and no orphans are left behind. Verified on a Linux service-managed fleet worker: canceling the `CancelThroughShim` step below produced + +```text +INTERRUPT: Sending signal "term" to process 39247 +demosleep: caught SIGTERM, exiting +``` + +Applications that install their own signal handlers still get the chance to shut down cleanly. Give `cancelation` a `NOTIFY_THEN_TERMINATE` mode in your step if a tool needs a grace period. + +## A future specification change removes the need for this + +This environment is a workaround for a gap in the environment specification rather than a permanent design. [RFC0008: Environment Wrap Actions](https://github.com/OpenJobDescription/openjd-specifications/issues/132) proposes `onWrapTaskRun`, letting a queue environment wrap each task's command directly instead of exporting variables to it. Once the worker agent supports that hook, it replaces both the shim directory and the `PATH` manipulation, and the tradeoffs above go away. The RFC has reached final comments upstream. + +## Cleanup + +The Rez installation, the `.rxt` context, and the shims are written under the session working directory and removed with the session. The demo package repository is not: it is created at `RezDemoRepository`, which defaults to `/tmp/rez-demo-repository` and persists on the worker until the instance is replaced. Delete it if you are testing on a long-lived worker, and detach the setup environment when finished. diff --git a/queue_environments/rez_shim/demo_job_bundle/template.yaml b/queue_environments/rez_shim/demo_job_bundle/template.yaml new file mode 100644 index 00000000..c6c09998 --- /dev/null +++ b/queue_environments/rez_shim/demo_job_bundle/template.yaml @@ -0,0 +1,138 @@ +specificationVersion: 'jobtemplate-2023-09' +name: Rez shim demo +description: | + Demonstrates that a Rez-provided tool can be invoked by bare command name + from a job template, with the Rez environment applied by the queue + environment's PATH shims rather than by replaying captured variables. +parameterDefinitions: + - name: ToolName + type: STRING + description: The Rez-provided command to invoke by bare name. + default: "demorender" + - name: CancelSleepSeconds + type: INT + description: > + How long the CancelThroughShim step sleeps. Cancel the job while that step + runs to confirm the signal reaches the tool through its shim. + default: 600 + minValue: 1 + +steps: +- name: RunRezTool + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: "1-2" + script: + actions: + onRun: + command: "{{Param.ToolName}}" + args: ["frame-{{Task.Param.Frame}}"] + +- name: VerifyEnvironment + dependencies: + - dependsOn: RunRezTool + script: + actions: + onRun: + command: bash + args: ["{{Task.File.Verify}}"] + embeddedFiles: + - name: Verify + filename: verify.sh + type: TEXT + data: | + set -uo pipefail + + echo "=== PATH ===" + echo "$PATH" | tr ':' '\n' | head -3 + + echo "=== Shim directory ===" + echo "REZ_SHIM_DIR=${REZ_SHIM_DIR:-UNSET}" + echo "REZ_CONTEXT_FILE=${REZ_CONTEXT_FILE:-UNSET}" + ls -l "${REZ_SHIM_DIR:-/nonexistent}" || echo "no shim dir" + + echo "=== Which resolves to a shim ===" + command -v "${1:-demorender}" || true + + # The tool's own environment variables must be visible INSIDE the tool's + # process, supplied by Rez, even though they were never exported into the + # session via openjd_env. + echo "=== DEMOTOOL_VERSION as seen by the session (expected UNSET) ===" + echo "DEMOTOOL_VERSION=${DEMOTOOL_VERSION:-UNSET}" + + echo "=== DEMOTOOL_VERSION as seen inside the tool (expected 1.0.0) ===" + demorender check-env + + # The three checks below are the point of the sample. Each one passes + # with the shim environment and fails with an environment that harvests + # variables from a subshell and replays them. + FAILURES=0 + + # Capture output into a variable before matching. Piping directly into + # `grep -q` makes grep exit at the first match, which closes the pipe and + # kills the writer with SIGPIPE (exit 141); under `pipefail` that reads + # as a failed check even though the output was correct. + echo "=== Check 1: plain variable reaches the tool ===" + tool_out="$(demorender check-env)" + if printf '%s\n' "$tool_out" | grep -q "DEMOTOOL_VERSION=1.0.0"; then + echo "PASS: variable visible inside the tool" + else + echo "FAIL: variable missing inside the tool" + FAILURES=$((FAILURES + 1)) + fi + + # Checks 2 and 3 both read `demoprobe`, which is a Rez-declared tool and + # so has a shim of its own. Calling it by bare name means the result + # depends on the shim mechanism working: if PATH injection were broken, + # `demoprobe` would not be found and both checks would fail. Probing the + # saved context directly with `rez env --input` would instead pass even + # with the shims entirely absent. + probe_out="$(demoprobe 2>/dev/null || true)" + + # An `alias` in a Rez package becomes an exported shell function. Bash + # exports it as BASH_FUNC_demoalias%%, a name containing '%%' with a + # multiline value, which openjd_env cannot express, so replaying captured + # variables drops it. The alias is not itself a command on PATH; the + # probe reports whether it is callable from inside the context. + echo "=== Check 2: Rez alias survives into the task ===" + if printf '%s\n' "$probe_out" | grep -q "alias=demorender: DEMOTOOL_VERSION"; then + echo "PASS: alias is callable" + else + echo "FAIL: alias was lost" + FAILURES=$((FAILURES + 1)) + fi + + # demotool provides its own `sort` and prepends its bin directory to + # PATH, so inside a correctly applied context it must shadow the system + # binary. This checks ordered PATH edits, not just PATH's contents. + echo "=== Check 3: package PATH prepend shadows the system command ===" + if printf '%s\n' "$probe_out" | grep -q "sort-shadowed=yes"; then + echo "PASS: package command shadows the system one" + else + echo "FAIL: system command won, PATH order was not preserved" + FAILURES=$((FAILURES + 1)) + fi + + echo "=== Result ===" + if [ "$FAILURES" -ne 0 ]; then + echo "openjd_fail: $FAILURES of 3 environment fidelity checks failed." + exit 1 + fi + echo "All 3 environment fidelity checks passed." + +# Optional manual check. This step sleeps inside a shimmed Rez tool so an +# operator can cancel the job and confirm the signal reaches the tool rather +# than stopping at the shim or leaving an orphan behind. It is not part of the +# automated fidelity checks because it needs someone to trigger the cancel. +- name: CancelThroughShim + dependencies: + - dependsOn: VerifyEnvironment + script: + actions: + onRun: + command: demosleep + args: ["{{Param.CancelSleepSeconds}}"] + cancelation: + mode: NOTIFY_THEN_TERMINATE diff --git a/queue_environments/rez_shim/rez_demo_setup_queue_env.yaml b/queue_environments/rez_shim/rez_demo_setup_queue_env.yaml new file mode 100644 index 00000000..9479dd58 --- /dev/null +++ b/queue_environments/rez_shim/rez_demo_setup_queue_env.yaml @@ -0,0 +1,137 @@ +specificationVersion: 'environment-2023-09' +# Test scaffolding for rez_queue_env_shim.yaml. +# +# Installs Rez and builds a small demo Rez package so the shim environment can +# be exercised on a worker that has neither. Attach this at a LOWER priority +# number than the shim environment so it runs first, and set the shim +# environment's RezRepositories parameter to the RezDemoRepository path below. +# +# This is for trying out and testing the shim environment. A production farm +# provides Rez on the worker image and a package repository on shared storage, +# and does not need this environment at all. +parameterDefinitions: + - name: RezDemoRepository + type: STRING + description: > + Directory to create the demo Rez package repository in. Pass this same + value as the shim environment's RezRepositories parameter. + default: "/tmp/rez-demo-repository" + userInterface: + control: LINE_EDIT + label: Rez Demo Repository + +environment: + name: RezDemoSetup + script: + actions: + onEnter: + command: "bash" + args: ["{{Env.File.Enter}}"] + embeddedFiles: + - name: Enter + filename: rez-demo-setup-enter.sh + type: TEXT + data: | + set -euo pipefail + + # The shim environment is tested on Linux and macOS only. + case "$(uname -s)" in + Linux|Darwin) ;; + *) + echo "openjd_fail: This demo supports Linux and macOS only (found $(uname -s))." + exit 1 + ;; + esac + + REPO='{{Param.RezDemoRepository}}' + REZ_INSTALL_DIR="{{Session.WorkingDirectory}}/rez-install" + + # Install Rez into the session directory so it is removed with the + # session. A production farm has Rez on the worker image instead. + echo "Installing Rez into the session directory..." + python3 -m venv "$REZ_INSTALL_DIR" + "$REZ_INSTALL_DIR/bin/pip" install --quiet rez + + # Build a demo package that exercises three kinds of environment state, + # in increasing order of how badly a variable diff handles them: + # + # 1. A plain variable. Harvest-and-replay carries this correctly. + # 2. An `alias`, which Rez implements as an exported shell function. + # Bash exports it as `BASH_FUNC_name%%`, whose name contains `%%` + # and whose value spans lines. The Open Job Description + # `openjd_env` directive accepts neither, so the alias is dropped. + # 3. A PATH prepend that shadows a system command. Replay restores the + # variable, but any later environment that also edits PATH can + # reorder it, so which binary wins depends on environment order. + mkdir -p "$REPO/demotool/1.0.0/bin" + cat > "$REPO/demotool/1.0.0/package.py" <<'PKG' + name = "demotool" + version = "1.0.0" + tools = ["demorender", "democonvert", "demosleep", "demoprobe"] + build_command = False + + def commands(): + env.PATH.prepend("{root}/bin") + env.DEMOTOOL_VERSION = "1.0.0" + env.DEMOTOOL_LICENSE_SERVER = "license.example.internal:1234" + # Rez turns this into an exported shell function, which cannot be + # represented as an `openjd_env` name=value pair. + alias("demoalias", "demorender --via-alias") + PKG + + cat > "$REPO/demotool/1.0.0/bin/demorender" <<'TOOL' + #!/usr/bin/env bash + echo "demorender: DEMOTOOL_VERSION=${DEMOTOOL_VERSION:-UNSET}" + echo "demorender: DEMOTOOL_LICENSE_SERVER=${DEMOTOOL_LICENSE_SERVER:-UNSET}" + echo "demorender: args=$*" + TOOL + cat > "$REPO/demotool/1.0.0/bin/democonvert" <<'TOOL' + #!/usr/bin/env bash + echo "democonvert: DEMOTOOL_VERSION=${DEMOTOOL_VERSION:-UNSET} args=$*" + TOOL + # Reports on state that only exists inside a correctly applied context: + # the alias Rez defines, and whether the package's own `sort` shadows the + # system one. Because this is a declared tool it gets a shim of its own, + # so the verify step can probe both through the shim mechanism rather + # than by re-entering the context itself. + cat > "$REPO/demotool/1.0.0/bin/demoprobe" <<'TOOL' + #!/usr/bin/env bash + # Rez exports an alias as a shell function, which is inherited here. + if type demoalias > /dev/null 2>&1; then + echo "demoprobe: alias=$(demoalias)" + else + echo "demoprobe: alias=MISSING" + fi + # `sort` must resolve to the package's copy, not /usr/bin/sort. + echo "demoprobe: sort=$(command -v sort)" + if sort --help 2>&1 | grep -q "demotool's sort"; then + echo "demoprobe: sort-shadowed=yes" + else + echo "demoprobe: sort-shadowed=no" + fi + TOOL + # A long-running tool that reports the signals it receives, used by the + # CancelThroughShim step to prove cancelation reaches through a shim. + cat > "$REPO/demotool/1.0.0/bin/demosleep" <<'TOOL' + #!/usr/bin/env bash + trap 'echo "demosleep: caught SIGTERM, exiting"; exit 42' TERM + trap 'echo "demosleep: caught SIGINT, exiting"; exit 43' INT + echo "demosleep: pid=$$ sleeping for ${1:-600}s" + # Sleep in short increments so the trap runs promptly. A single long + # sleep would not be interrupted until it returned. + for _ in $(seq 1 "${1:-600}"); do sleep 1; done + echo "demosleep: finished without receiving a signal" + TOOL + # A package-provided `sort` that shadows the system one. The package + # directory is prepended to PATH, so inside a correctly applied context + # this must win over /usr/bin/sort. + cat > "$REPO/demotool/1.0.0/bin/sort" <<'TOOL' + #!/usr/bin/env bash + echo "sort: this is demotool's sort, not the system one" + TOOL + chmod +x "$REPO/demotool/1.0.0/bin/"* + + echo "Created demo Rez package 'demotool-1.0.0' in $REPO" + + # Put Rez on PATH so the shim environment's plain `rez` calls resolve. + echo "openjd_env: PATH=$REZ_INSTALL_DIR/bin:$PATH" diff --git a/queue_environments/rez_shim/rez_queue_env_shim.yaml b/queue_environments/rez_shim/rez_queue_env_shim.yaml new file mode 100644 index 00000000..9efa1420 --- /dev/null +++ b/queue_environments/rez_shim/rez_queue_env_shim.yaml @@ -0,0 +1,168 @@ +specificationVersion: 'environment-2023-09' +parameterDefinitions: + - name: RezPackages + type: STRING + description: > + This is a space-separated list of Rez packages to install for the job. + E.g. "blender-3.6" for a job that renders frames in Blender 3.6. + default: "" + userInterface: + control: LINE_EDIT + label: Rez Packages + - name: RezRepositories + type: STRING + description: > + This is a ':'-separated list of Rez repositories from which to install + packages. + # Edit the script code below to set the Linux and MacOS repository path, + # it will search/replace the token REZ_REPOSITORY_PATH in the default to that. + default: "REZ_REPOSITORY_PATH" + userInterface: + control: LINE_EDIT + label: Rez Repositories + - name: RezExtraTools + type: STRING + description: > + Optional space-separated list of extra command names to create shims for, + in addition to the tools Rez reports for the resolved packages. Use this + for commands a package provides but does not declare in its "tools" list. + default: "" + userInterface: + control: LINE_EDIT + label: Rez Extra Tools + +environment: + name: Rez + script: + actions: + onEnter: + command: "bash" + args: ["{{Env.File.Enter}}"] + embeddedFiles: + - name: Enter + filename: rez-queue-env-enter.sh + type: TEXT + data: | + set -euo pipefail + + if [ -z '{{Param.RezPackages}}' ]; then + echo "Skipping Rez env as RezPackages parameter was empty." + exit 0 + fi + + # The shims this environment writes are POSIX shell scripts that rely on + # a shebang line to be executable. That does not work on Windows, so + # fail with a clear message rather than producing shims that cannot run. + case "$(uname -s)" in + Linux|Darwin) ;; + *) + echo "openjd_fail: This queue environment supports Linux and macOS only (found $(uname -s))." + echo "Use rez_queue_env.yaml on Windows workers." + exit 1 + ;; + esac + + # Edit these paths for your farm + LINUX_REZ_REPOSITORY_PATH="/mnt/REZ_REPOSITORY" + MACOS_REZ_REPOSITORY_PATH="/Volumes/REZ_REPOSITORY" + + REZ_PACKAGES='{{Param.RezPackages}}' + REZ_REPOSITORIES='{{Param.RezRepositories}}' + REZ_EXTRA_TOOLS='{{Param.RezExtraTools}}' + + if [[ "$(uname)" == Darwin ]]; then + REZ_REPOSITORY_PATH="$MACOS_REZ_REPOSITORY_PATH" + else + REZ_REPOSITORY_PATH="$LINUX_REZ_REPOSITORY_PATH" + fi + REZ_REPOSITORIES="${REZ_REPOSITORIES//REZ_REPOSITORY_PATH/$REZ_REPOSITORY_PATH}" + + echo "Rez Package List:" + echo " $REZ_PACKAGES" + + # Resolve the Rez context once and save it to the session directory. Each + # task re-enters this saved context instead of re-resolving, so every task + # sees an identical environment and pays no resolve cost. + # + # Unlike the environment-variable capture approach in rez_queue_env.yaml, + # the resolved context is applied by Rez itself inside each task's own + # process. Shell functions, aliases and ordered PATH edits that a plain + # variable diff cannot represent are therefore preserved. + CTX="{{Session.WorkingDirectory}}/rez-context.rxt" + rez env --paths "$REZ_REPOSITORIES" $REZ_PACKAGES --output "$CTX" + + # Ask Rez which executables the resolved packages provide, so the shims + # below do not need a hard-coded list of tool names. `rez context -t` + # prints a two-line header followed by "TOOL PACKAGE" rows. + REZ_TOOLS="$(rez context -t "$CTX" | awk 'NR>2 && NF {print $1}' | sort -u)" + + SHIM_DIR="{{Session.WorkingDirectory}}/rez-shims" + mkdir -p "$SHIM_DIR" + + SHIM_COUNT=0 + SHIMMED_TOOLS="" + SKIPPED_TOOLS="" + for TOOL in $REZ_TOOLS $REZ_EXTRA_TOOLS; do + # Skip Rez's own commands. Shimming them would mean `rez` inside a + # task recursed back through the shim instead of running Rez. + case "$TOOL" in + rez|rez-*) continue ;; + esac + + # Resolve the tool to an absolute path inside the context, so each + # shim execs the real binary rather than a bare name. + # + # A bare name would be re-resolved against whatever PATH exists when + # the shim runs. Rez normally rebuilds PATH from the context and + # drops SHIM_DIR, so a bare name is usually safe -- but a farm whose + # rez config lists PATH in `parent_variables` keeps SHIM_DIR ahead of + # the package's own bin directory, and the shim would then re-invoke + # itself and fork until the worker runs out of processes. Resolving + # once here removes that possibility regardless of Rez configuration. + TOOL_PATH="$(rez env --input "$CTX" --shell bash -- \ + bash -c "command -v -- '$TOOL'" 2>/dev/null | tail -n 1 || true)" + + # Refuse anything that resolves back into the shim directory, or that + # the context cannot resolve at all. Either would loop or fail + # confusingly at task time, so report it now instead. + case "$TOOL_PATH" in + "$SHIM_DIR"/*|"") + SKIPPED_TOOLS="$SKIPPED_TOOLS $TOOL" + continue + ;; + esac + + # Each shim re-enters the resolved context in a real shell and then + # execs the tool by absolute path, forwarding all arguments. `exec` + # avoids leaving an extra shell in the process tree; Rez still starts + # a shell of its own, and signals reach the tool through it. + cat > "$SHIM_DIR/$TOOL" <