Install
openclaw skills install @loonghao/rez-package-authoringRules and decision-making for authoring Rez packages — how to split versions and variants, how to write requires/variants/build_requires/commands, how wide to make dependency ranges, the rez-build and rez-release workflow and conventions, and the recurring pitfalls and anti-patterns. Use when writing, reviewing or restructuring a package.py, or when deciding whether a change needs a new version or a new variant. Covers Rez 3.4.0.
openclaw skills install @loonghao/rez-package-authoringOne-sentence summary:
package.pyis a declaration of what a package needs and provides — decide the version/variant split first, then keep requirement ranges as wide as the truth allows and as narrow as reality demands.
Skill scope: how to decide and structure a package. For the attribute reference and
@early/@late mechanics see rez-package-definition; for the commands() body and rex API see
rez-package-commands; for build/release command flags see rez-cli; for what rez does to the
module scope of package.py — serialization, build-time freezing and the errors that follow — see
rez-package-pitfalls.
Get these right and the file writes itself.
| The change | Do this |
|---|---|
| Your own source code changed | new version |
| A dependency version it must build against changed (maya 2016 → 2017, python 2 → 3) | new variant |
| Need to support a second platform/arch/os | new variant |
| Fixing a bug in the payload | new version |
The rule that catches people out: variants cannot be added to a package that has none without
bumping the version. A package released with no variants at all can never gain them. So if a
compiled package plausibly needs variants later — a second python, a second DCC — declare the single
variant now:
name = "my_utils"
variants = [
["python-2.7"],
]
This is the "future proofing" reason for single-variant packages. The second common reason is
cosmetic but real: variant requirements appear in the install path, so
my_utils/1.0.0/python-2.7/<payload> tells a user what they are getting, where
my_utils/1.0.0/<payload> does not.
Variants are per-version, not per-package. Adding a variant means re-releasing that version; it does not retroactively change already-released versions.
Start from the truth about compatibility, then express it with the narrowest syntax that still says it.
| You mean | Write |
|---|---|
| any version | foo |
any 1.x | foo-1 |
1.0.0 or newer | foo-1+ |
at least 1.2, below 2 | foo-1.2+<2 |
exactly 2.0.0, nothing else | foo==2.0.0 |
1.3 or 5+, but not between | foo-1.3|5+ |
| if present it must be in range, but don't require it | ~foo-2.7.3 |
| this version is unacceptable | !foo-2015.6 |
Three rules for sound ranges:
Do not pin what you do not have to pin. python==2.7.11 forces every environment that uses
your package onto that exact build; python-2.7 lets rez co-exist with everything else. Pin only
when there is a concrete incompatibility.
Do not widen past what you tested. A range is a promise. foo-1+ claims you work with every
future foo-1.
Build time can be wider than runtime. This is what requirements expansion is for. A C++ package
may build against any boost-1 but must link at runtime against the minor version it was built
with. Write the loose range and let rez expand it:
requires = [
"boost-1.*", # expands to the version actually built against, e.g. boost-1.55
]
** expands to the full version (boost-1.** → boost-1.55.1). The equivalent done by hand:
@early()
def requires():
from rez.package_py_utils import expand_requires
return expand_requires(["boost-1.*"])
Use ~ (weak reference) when you constrain but do not require. DCCs that ship an embedded
python should declare requires = ["~python-2.7.3"]: python is not pulled into the environment,
but if something else brings python in, it has to be the compatible one. This is how a studio keeps
python libraries usable both inside and outside the DCC.
| Attribute | Puts into the environment at | Transitive? |
|---|---|---|
requires | runtime and build | yes |
build_requires | build only | yes — collects from every package in the build env |
private_build_requires | build only | no — only from the package being built |
variants | build (one variant at a time) and runtime (the selected variant) | n/a |
Choose build_requires when other packages also need the dependency to build against your headers
— a header-only C++ library is the canonical case: if your public headers include its headers, anyone
compiling against you needs it too. Choose private_build_requires for things nobody downstream
needs: doc generators (doxygen, sphinx), build utilities, statically linked libraries.
The build environment is assembled in a fixed order — requires, then transitive build_requires,
then private_build_requires, then the current variant's requirements.
A subtle trap with building: early-bound functions are evaluated more than once per build —
once pre-build, and once per variant — and the pre-build value (where building is False) is
the one baked into the installed package. So to add a requirement at runtime only:
@early()
def requires():
if building:
return ["python-2"]
else:
return ["runtimeonly-1.2", "python-2"]
Always check the function returns what you want when building is False.
commands()Rez is not a build system and does not know how your package is consumed. commands() is where you
say it. Keep it declarative — remember it is interpreted into shell code, not executed as Python.
def commands():
env.PATH.append("{root}/bin")
env.PYTHONPATH.append("{root}/python")
if building:
env.CMAKE_MODULE_PATH.append("{root}/cmake")
Rules:
if building: so consumers find headers and CMake config at build
time without polluting the runtime environment.commands() never runs for the package being built. Use pre_build_commands() for that.tools, not by hand-managing PATH, so rez-context --tools and the tool
conflict detection work.@early() attribute; use
@late() only for values that genuinely depend on runtime state (env vars, user role).rez-build --install # build all variants, install to ~/packages
rez-env mypackage # resolve and test in a separate shell
rez-test mypackage # run the package's tests
rez-release # build + release_hooks + VCS plugins + tag
Conventions worth following:
rez-build --install goes to local_packages_path
(typically ~/packages), which sits at the front of packages_path, so your local package shadows
the released one. That is the intended test loop — and also the reason --no-local exists when a
local build is masking a released one.--prefix to install somewhere else, for example
rez-build -i --prefix /path/to/repo. rez-release is the managed alternative: it targets
release_packages_path, runs release_hooks, runs tests, does sanity checks and VCS tagging, and
adds the release package attributes automatically.rez-build --view-pre prints the preprocessed package and exits —
the fastest way to see what preprocess() and @early() actually produced.rez-build --variants 0.--: rez-build -- -DMYVAR=YES.| Anti-pattern | Why it breaks | Do instead |
|---|---|---|
foo==1.2.3 on every dependency | over-constrains every downstream resolve | range it: foo-1.2+<2, pin only real incompatibilities |
No variants on a package that will need them | variants cannot be added later without a version bump | declare one variant now |
| Non-mutually-exclusive variants, relying on the pick | rez cannot compare versions across packages; the choice is a preference, and a transitive dep can overrule it | put the discriminator in the request, e.g. rez-env pkg maya |
Heavy work in @late() | runs at resolve/runtime, on every use | compute at build time in @early(), keep @late() for genuine runtime state |
Imports at the top of package.py for @late() | late functions must import inside the function | import inside the function body |
Referencing other @early()/@late() attributes from an @early() function | raises an error | reference plain attributes only, via this |
Expecting commands() to run during your own build | it never runs for the package being built | use pre_build_commands() |
Referencing files by relative path in commands() | cwd during a build is the build path, not the package root | use {root} |
| Editing a released package in place | breaks timestamped resolves and returns stale cached resolves | release a new version |
| Setting env vars for build consumers unconditionally | pollutes runtime | guard with if building: |
Skipping tools and appending to PATH by hand | loses tool tracking and conflict detection | use the tools attribute |
For the ones that produce a rez error rather than a bad decision — top-level imports that make the
installed package.py unparseable, values frozen on the build machine — see
rez-package-pitfalls, which carries the verbatim error text.
Before releasing, check in this order:
build_requires / private_build_requires, and is the transitive
one the right choice?@early() function return the right value when building is False?commands() guarded by if building:?rez-build --view-pre show the package you intended?rez-test pass before rez-release?package.py and rez-build --view-pre output before editing anything.requires.~ and ! over hard requirements when you are constraining rather than requiring.rez-build --view-pre, then rez-build --install, then rez-env in a separate
shell — cheapest check first.rez-test before rez-release.