Rez package commands and the rex API
One-sentence summary:
commands()is Python that configures the environment; it is translated into shell code, not executed as Python at runtime.
Skill scope: the commands() body. For the surrounding package.py see rez-package-definition.
How it runs
commands() is not plain Python at run time. Rez interprets it and emits target-shell code
(bash, zsh, csh, tcsh, sh, pwsh). Consequences:
- Only the documented rex API has meaning — arbitrary Python side effects do not survive.
print()insidecommands()does not write to the user's terminal in the way you expect; useinfo()/error().- Imports must be inside the function body.
The env object
The env object is the common case and reads naturally:
def commands():
env.PATH.append("{root}/bin")
env.PYTHONPATH.append("{root}/python")
env.FOO_LIC = "{this.root}/lic" # plain assignment == setenv
env.MAYA_PLUG_IN_PATH.prepend("{root}/maya")
env.SOME_VAR.unset()
Equivalent function-style API:
env form |
rex function | Effect |
|---|---|---|
env.X = v |
setenv("X", v) |
set (overwrite) |
env.X.append(v) |
appendenv("X", v) |
append to path-like var |
env.X.prepend(v) |
prependenv("X", v) |
prepend to path-like var |
env.X.unset() |
unsetenv("X") |
unset |
| — | alias("name", "value") |
create a shell alias |
| — | source("path") |
source a script |
| — | error("msg") |
raise a user-visible error |
| — | info("msg") |
print a message |
| — | defined("VAR") |
True if the env var is set |
| — | expandvars("{root}/bin") |
expand immediately |
The first append overwrites
The first append/prepend on a given variable actually overwrites it. This is
deliberate: otherwise a system PyQt already on PYTHONPATH would still win over the version you
just resolved.
PATH is special-cased — it is not clobbered, because you would lose ls/cd. The system paths
(from a non-interactive shell's default PATH) are appended back after all commands are
interpreted.
String expansion
Three flavors, and the difference matters:
| Package command | Equivalent bash |
|---|---|
env.FOO = literal("${USER}") |
export FOO='${USER}' |
env.FOO = expandable("${USER}") |
export FOO="${USER}" |
env.FOO = expandvars("${USER}") |
export FOO="jbloggs" |
- Object expansion (
{root},{this.root},{this.version.major}) and env-var expansion ($FOO,${FOO}) normally happen automatically when a string reaches a rex call orenv. - A bare
var = "{root}/bin"does not expand — wrap it:var = expandvars("{root}/bin"). literal()inhibits expansion; chain with.expandable()for mixed content:
env.DESC = literal("the value of {root} is").expandable("{root}")
Filepaths — always POSIX, even on Windows
def commands():
env.PATH.append("{root}/bin") # forward slash, even on Windows
Rez normalizes to the target shell. It knows which variables are paths via the pathed_env_vars
config setting — by default any variable ending in PATH.
Never use os.pathsep or hardcoded a:b lists. On Git-for-Windows (git-bash) the shell's path
separator differs from the underlying system's, so these break portability.
Order of execution
Two rules, in this precedence:
- If package
Awas requested beforeB,A's commands run beforeB's. - Unless
ArequiresB— thenBruns first.
So for rez-env maya_anim_tool-1.3+ PyYAML-3.10 maya-2015, where maya_anim_tool requires maya
and PyYAML requires python:
maya → maya_anim_tool → python → PyYAML
This is why a Maya plugin can safely append to MAYA_PLUG_IN_PATH — Maya initialized it first.
Branching on context
Objects available in commands():
| Object | Use |
|---|---|
root / this.root |
install path of current variant |
base / this.base |
parent of the variant dirs |
this.version |
version, with .major / .minor / .patch |
building |
True during a build |
resolve |
mapping of resolved packages; "maya" in resolve |
ephemerals |
mapping of ephemeral packages |
in_context() |
True when part of a resolved context |
defined(name) |
env var presence |
A realistic example:
def commands():
import os.path # imports MUST be inline
env.PYTHONPATH.append("{this.root}/python")
env.PATH.append("{this.root}/bin")
if building: # expose headers only to builds
env.FOO_INCLUDE_PATH = "{this.root}/include"
if defined("DEBUG_FOO"):
conf_file = os.path.expanduser("~/.foo/config")
else:
conf_file = "{this.root}/config"
env.FOO_CONFIG_FILE = conf_file
if "maya" in resolve: # only wire up maya bits when maya is present
env.MAYA_PLUG_IN_PATH.append("{this.root}/maya/plugins")
if resolve.maya.version.minor == "sp3":
error("known issue with GL renderer in service pack 3, beware")
env.FOO_LIC = "/lic/foo_{this.version.major}.lic"
Build-time communication via building
Dependency packages use building to expose build-only information:
# in boost's package.py
def commands():
if building:
env.CMAKE_MODULE_PATH.append("{root}/cmake") # ships FindBoost.cmake
commands()is never executed for the package actually being built. Usepre_build_commands()when you need code to run for the package under construction.
Pre and post commands
pre_build_commands()— runs for the package being built, before the build.pre_test_commands()— runs before the package's tests.
Both take the same rex API as commands().
Review checklist
- Imports are inside the function body.
- Forward slashes everywhere; no
os.pathsep, no hardcoded:-joined paths. - Guards use
building/in_context()/"pkg" in resolverather than assuming. - The first
appendon a variable is understood to overwrite. -
literal()used where a$VARmust survive into the shell unresolved.
Debug a commands section
rez-env mypkg --verbose # more solver/command detail
rez-context --interpret # show interpreted shell code
rez-test mypkg # run the package's own tests
rez-env mypkg -- printenv MY_VAR # one-shot check without a subshell
