Install
openclaw skills install @loonghao/rez-package-pitfallsThe package.py execution model and the errors it produces — why a top-level from x import SomeClass makes the installed package.py unparseable, why module-scope values are frozen on the build machine, why env/this/root look undefined to linters, and why a failed rez-build can leave a broken install behind. Use when a package resolves or builds with a confusing error, when reviewing a package.py for module-scope mistakes, or before releasing one. Covers Rez 3.4.0.
openclaw skills install @loonghao/rez-package-pitfallspackage.py pitfallsOne-sentence summary: rez
execspackage.pyonce at build time and writes every surviving module-level name back out as Python source, so anything at module scope that is not a plain value, a function or a module lands in the installedpackage.pyas text that no longer parses.
Skill scope: what rez does to package.py and when. For the attribute reference and the
@early/@late mechanics see rez-package-definition; for the commands() body and the rex API
see rez-package-commands; for the version/variant/range decisions and the rest of the
anti-pattern list see rez-package-authoring.
Every pitfall below was reproduced against Rez 3.4.0, and the error text is copied from a real run with only the host, user and timestamp replaced by placeholders.
| Phase | When it runs | What it does to package.py |
|---|---|---|
| Load | every time rez reads a package, including during rez-build | execs the file; every module-level name is collected as a candidate attribute |
| Serialize | on rez-build / rez-release | writes the surviving attributes back out as a new package.py into the install |
| Interpret | on rez-env and every other resolve | runs commands() through rex and emits shell code; does not run the module body |
Two consequences drive every pitfall here: the module body is Python evaluated on the build
machine, and commands() is not Python at resolve time.
from x import SomeClass breaks the installed packagerez-build --install fails while resolving the build environment, before your build command ever
runs. The named file is a temporary copy, not your source.
Any module-level name bound to something that is not a plain value, a function, a module or a
__-leading name. The most common shape is importing a class at the top of the file:
from pathlib import Path # Path is a class, so it survives serialization
documents_path = Path.home().as_posix() + "/Documents/.p4config"
name = "foo"
version = "1.0.0"
Resolving build environment:
resolved by builder@BUILD-HOST, on Fri Oct 02 01:05:17 2026, using Rez v3.4.0
requested packages:
~platform==windows (implicit)
~arch==AMD64 (implicit)
~os==windows-10.0.26100.SP0 (implicit)
resolved packages:
01:05:17 ERROR ResourceError: Problem loading C:\Users\<user>\AppData\Local\Temp\rez_write__epzwl2x\package.py: invalid syntax (package.py, line 12)
Invoking custom build system...
The rez_write_<random> directory is rez's own temp copy of the definition
(rez/serialise.py, prefix="rez_write_"), so the path in the message never points at the file you
edited. The offending line is what rez wrote, not what you wrote — here line 12 of the generated
file is:
Path = <class 'pathlib.Path'>
package.py attributes are dumped with pformat(), and the repr of a class object is not valid
Python source.
Import inside commands(), and keep the value out of the module scope entirely:
name = "foo"
version = "1.0.0"
def commands():
# Import inside commands() so the module is not captured as a package
# attribute: rez serializes module-level names into the installed
# package.py, and a class object renders as invalid syntax.
from pathlib import Path
documents_path = Path.home().as_posix() + "/Documents/.p4config"
env.P4CONFIG.set(documents_path)
The installed package.py then carries name, version, commands, timestamp and
format_version — and nothing else. The last two are rez's, not yours: timestamp defaults to the
install time and format_version is always written by the filesystem package repository
(rezplugins/package_repository/filesystem.py), so expect both after every rez-build --install.
A package works on the machine that released it and is wrong everywhere else: a per-user path, a
hostname, a sys.prefix or a os.getcwd() result points at the builder's machine.
Computing anything environment-dependent at module scope:
import sys
from pathlib import Path
sys_path = sys.prefix # frozen at build time
documents_path = Path.home().as_posix() + "/Documents/.p4config"
The module body runs once, during rez-build, and its results are written into the install.
This is what lands in the installed package.py:
sys_path = 'C:\\Users\\<user>\\AppData\\Local\\Programs\\Python\\Python312'
documents_path = 'C:/Users/<user>/Documents/.p4config'
Note that sys_path survived even though sys itself did not — the module was stripped, the string
it produced was not.
Move anything that depends on who or where the resolve runs into commands(), which is
interpreted on the consumer's machine at resolve time:
def commands():
from pathlib import Path
env.P4CONFIG.set(Path.home().as_posix() + "/Documents/.p4config")
Use @early() only for values that are genuinely a property of the build — see
rez-package-definition.
env, this and root are undefined names to your linterYour editor or CI linter flags package.py even though it works perfectly in rez.
Any use of a name that rez injects. Rez binds this, version, root and base only while it
interprets commands() (resolved_context.py), and env is the rex environment object. None of
them exists when the file is read as ordinary Python.
F821 Undefined name `env`
--> package.py:6:5
|
5 | def commands():
6 | env.PYTHONPATH.prepend("{this.root}/site-packages")
| ^^^
Silence it at the point of use, which documents the intent for the next reader:
env.PYTHONPATH.prepend("{this.root}/site-packages") # noqa: F821
env.RESOURCE_PATH.prepend("{this.root}/resource") # noqa: F821
Do not "fix" it by defining env or root yourself at module scope — that shadows rez's binding and
creates a real package attribute that then gets serialized (pitfall 1 and 2).
You fix package.py, rebuild, and the same error comes back — now naming the installed copy
instead of a temp file.
A failed rez-build --install leaves a package behind whenever the run got as far as writing the
install. Whether it did depends on where the build stopped: the failure in pitfall 1 happens while
resolving the build environment, before the build system is invoked, so that run installs nothing —
but a failure during or after the install step does leave the payload in place. When an install is
left behind, every later resolve and every later build of that package fails while reading it,
whatever your source now says.
Check whether the failed run left an install behind before deleting anything — the answer is not always yes:
rez-search foo # is foo visible, and at which version?
If it is, remove that version and rebuild:
rm -rf <packages_path>/foo/1.0.0
rez-build --install
Then verify in a fresh resolve, not in the build shell:
rez-env foo
rez-context --so
Reproduced on 3.4.0 by loading each snippet and dumping the installed form:
| Module-level statement | Survives? | Written as |
|---|---|---|
import sys | no — module stripped | — |
sys_path = sys.prefix | yes | 'C:\\Users\\<user>\\...\\Python312' (build machine) |
from os.path import join | no — function stripped | — |
def _helper(): ... | no — plain function stripped | — |
from pathlib import Path | yes | Path = <class 'pathlib.Path'> — unparseable |
class Helper: ... | yes | Helper = <class 'Helper'> — unparseable |
my_re = re.compile(r'foo-\d+') | yes | my_re = re.compile('foo-\\d+') — parses, then NameError: name 're' is not defined |
my_const = 'hello' | yes | 'hello' — a normal attribute, this is the intended use |
The rule rez applies is narrow: it strips modules, functions (except commands,
pre_commands, post_commands, pre_build_commands, pre_test_commands, preprocess, @early
and @late) and __-leading names. Everything else stays.
Surviving values fail in two different ways, and which one you get tells you where to look. A
repr that is not valid Python (Path = <class 'pathlib.Path'>) fails while parsing, with
invalid syntax. A repr that is valid Python but names a stripped module
(my_re = re.compile(...)) parses cleanly and fails while executing, with NameError. Both
reach you as ResourceError: Problem loading <path>: <reason>, so read the reason, not just the
exception type.
commands() or inside @late().Path.home(), os.getcwd(), sys.prefix, os.environ) is
computed at module scope.env / this / root use in commands() carries # noqa: F821 if the repo lints
package.py.env, this or root is defined at module scope to "satisfy" a linter.rez-build --install exits 0 and rez-env <pkg> resolves — a non-zero build may still
have written an install.rez-search <pkg> was used to check for a broken install, and
any that was found was removed before rebuilding.rez_write_* temp path means the generated package, a
packages_path path means an already-installed one.package.py for import, class, and anything that is not a literal
before suspecting the solver.commands(); move environment-dependent values into commands() too.rez-search <pkg>), remove it if
present, then rez-build --install.rez-env <pkg> in a separate shell — a green build proves the file parses, not that
it resolves.