Make rez wheels production ready
Make rez fully installable with pip, even for production uses. This takes some of the ideas in https://github.com/AcademySoftwareFoundation/rez/pull/1039 by @davidlatwe and improves them to make the whole concept work.
Included in the PR:
- Fully working pip installable wheels for production use.
- A forked and slightly modified simple launcher. The license is BSD-2.
- No more
.rez_production_install. - Completely removed the notion of production install from the code base.
- Integration tests that demonstrate that it really works (see the
wheel.yamlworkflow).
TODO:
- [ ] Implement the release of the new wheels.
- [ ] Include all this text into the docs, or at least in a README somewhere.
- [x] Should this somehow be feature flagged? Right now the install script will use all the new stuff...
- Changes have been made to make the install script use the old method.
What is a production install today?
Our install.py script does mainly three things, out of which two are to guarantee that rez will work in a production environment:
- Create a virtualenv and install rez into it (using
pip install .). - It copies all the commands (console entrypoints) from the virtualenv's "bin" folder into the "bin/rez" folder. This is necessary because we want to be able to add the commands to
$PATHbut we don't want thepythonexecutable from the virtualenv to be on PATH. We also can't remove thepythonexecutable from the bin folder because that would make it impossible to install rez plugins. - All scripts (commands) have their shebang modified from
#!/path/to/rez/venv/bin/pythonto#!/path/to/rez/venv/bin/python -E(we add-E) and we generate custom.exes on Windows to do the same thing.-Eis important for us because we don't want PYTHONPATH to affect the rez commands.
The goal of all this is to insulate the rez install from everything. Nothing should affect it. It must work under all conditions. Additionally, rez must not be affected by a rez environment. For example, if your rez install uses Python 3.7 and you rez-env python-3.12, running rez commands inside that new environment must use the Python 3.7 interpreter from the rez install! Rez must also ignore any environment variables that could affect it (PYTHONPATH, etc).
The install script is needed today because the Python ecosystem was not designed with these goals in mind. When you install a package with pip (or any other installer) and that package has commands (console entry points), installers will create small script wrappers that look like this:
#!/home/jcmorin/jcmenv/aswf/rez/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pytest import console_main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(console_main())
As you can see, it doesn't have the -E flag in the shebang. And most importantly, there is no way to tell
installers to use a custom shebang or where the scripts will be installed. They always go in bin (macOS, Linux) or Scripts (Windows).
The install.py script exists to fix all this. It was created because it was the easiest thing to do.
Why is it a problem to have to run the install.py script?
Python users are used to simply install things with their preferred installer (pip, etc). So it is natural that they would also want to decide how they want to install rez.
But with what we've seen above, they can't. Or more, they can but they'll see warnings every time they use the CLI.
We get asked quite often why can't they simply pip install rez.
It's very annoying to have to clone a repo and have to run a script. The alternative is to download an archive from a release and unpack it and run the install script. Both options are annoying IMO, and I'm not the only one annoyed by this based on all the discussions we've had on that subject in the last couple of years.
How do we fix this?
This PR utilizes the .data directory of the wheel format. The .data can contain a couple of folders, one of which is the scripts folder. The scripts folder is understood and supported by installers. Anything in scripts will go in <prefix>/bin (macOS, Linux) or <prefix>/Scripts (Windows) as is (or almost as is).
So what we do is generate the console scripts at build time and store them inside rez-<version>.data/scripts/rez in the wheel. When pip installs the wheel, it will unpack everything from <dist>-<version>.data/scripts into the bin folder and it will respect the hierarchy defined in the wheel! Pip will also replace the interpreter path found in the script's shebang with the venv's one. Again this is important because we want rez to use its own Python. So that's one problem solved (point 2 from above).
Example of what is inside the wheel
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-complete
657 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-install-test
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez_fwd
631 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-benchmark
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bind
643 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-build
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bundle
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-config
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-context
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-cp
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-depends
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-diff
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-env
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-gui
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-help
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-interpret
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-memcache
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-mv
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pip
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-cache
653 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-ignore
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-plugins
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-python
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-release
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-rm
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-search
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-selftest
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-status
643 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-suite
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-test
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-view
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-yaml2py
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rezolve
Now, how do we solve the fact that we can't easily add -E to the shebang? This is solved with this little trick in our custom console scripts:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import re
import sys
import platform
# If -E is not passed, then inject it and re-execute ourselves.
# Note that this is not done on Windows because the Windows launcher
# already does this.
if not sys.flags.ignore_environment and platform.system() != 'Windows':
args = [sys.executable, '-E'] + sys.argv
if os.getenv('REZ_LAUNCHER_DEBUG'):
print('Launching:', ' '.join(args))
# Re-execute ourself, this time with the -E flag.
os.execvp(sys.executable, args)
from rez.cli._entry_points import run_rez_build
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\\.pyw|\\.exe)?$', '', sys.argv[0])
sys.exit(run_rez_build())
This is the rez-build script that we store in our wheel. Note that pip will automatically replace /usr/bin/python by <absolute path to venv>/bin/python at install time.
Because we want to let pip bake the absolute path to the interpreter at install time, we can't pass any custom flags in there. If we were to specify #!/usr/bin/python -E, pip would not replace the path with the path we want.
So what we do is to check how the script was executed. It's possible to know if Python was started with -E or not by querying sys.flags.ignore_environment. If it was not started with -E, we re-exec ourselves with -E prepended to the arguments.
At least that's on Linux. On Windows, things are more complicated (as usual).
On Windows, we need executables to be able to just run "rez" or "rez-env", etc. Why? Because on Windows, you can't execute a random file without an extension. So you have to wrap the little Python script above with an executable. Pip usually takes care of that with entry points, but it doesn't do so for scripts, by design.
These executables are called "launchers" in the Python ecosystems. CPython has a built-in one, then there is the original simple launcher by Vinay Sajip and there is also a derivative of the simple launcher in distlib also by Vinay Sajip. There is a fourth one in setuptools, but let's ignore this one. Pip uses the one from distlib.
What I did was to take the one from distlib and put it into the launchers folder. I then patched it to fit our needs. The patch is small:
Diff
diff --git a/tmp/asd/launcher.c b/./launcher.c
index 727f7916..8265a2bb 100644
--- a/tmp/asd/launcher.c
+++ b/./launcher.c
@@ -23,6 +23,8 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
+// This file is taken from https://github.com/pypa/distlib/blob/0.3.7/PC/launcher.c.
+// You can diff the files to see the changes.
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
#endif
@@ -31,13 +33,9 @@
#include <io.h>
#include <stdlib.h>
#include <windows.h>
-#include <Shlwapi.h>
+#include <shlwapi.h>
-#pragma comment (lib, "Shlwapi.lib")
-
-#define APPENDED_ARCHIVE
-#define USE_ENVIRONMENT
-#define SUPPORT_RELATIVE_PATH
+#pragma comment (lib, "shlwapi.lib")
#define MSGSIZE 1024
@@ -822,6 +820,13 @@ run_child(wchar_t * cmdline)
#endif
si.dwFlags |= STARTF_USESTDHANDLES;
}
+
+ size_t rez_envvar_size = 0;
+ getenv_s(&rez_envvar_size, NULL, 0, "REZ_LAUNCHER_DEBUG");
+ if (rez_envvar_size > 0) {
+ printf("Launching: %ls\n", cmdline);
+ }
+
ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &child_process_info);
if (!ok) {
// Failed to create process. See if we can find out why.
@@ -1027,11 +1032,12 @@ process(int argc, char * argv[])
wcp = pbuffer;
}
#endif
- /* 3 spaces + 4 quotes + NUL */
- len = wcslen(wcp) + wcslen(wp) + 8 + wcslen(psp) + wcslen(cmdline);
+ /* 4 spaces + 4 quotes + -E + NUL */
+ len = wcslen(wcp) + wcslen(wp) + 11 + wcslen(psp) + wcslen(cmdline);
cmdp = (wchar_t *) calloc(len, sizeof(wchar_t));
assert(cmdp != NULL, "Expected to be able to allocate command line memory");
- _snwprintf_s(cmdp, len, len, L"\"%ls\" %ls \"%ls\" %ls", wcp, wp, psp, cmdline);
+ // Note that we inject -E to make sure PYTHON* variables are not picked up.
+ _snwprintf_s(cmdp, len, len, L"\"%ls\" -E %ls \"%ls\" %ls", wcp, wp, psp, cmdline);
run_child(cmdp); /* never actually returns */
free(cmdp);
return 0;
We basically
- Remove the
APPENDED_ARCHIVEpreprocessor directive so that the console script is not embedded in the EXE file. - Remove the
USE_ENVIRONMENTpreprocessor directive to not use any environment variables to resolve the path to python. - Remove the
SUPPORT_RELATIVE_PATHpreprocessor directive to make sure that the embedded python path doesn't use a relative path. - Add support for a new environment variable,
REZ_LAUNCHER_DEBUGthat will tell the launcher to print the full command line that will be used to start python (and run the console script). - Add
-Eto the arguments used when launching python.
We put the launchers in the wheel too. And this solves the last remaining problem we had.
Example of what is inside the wheel
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-complete-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-complete.exe
657 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-install-test-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez-install-test.exe
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez_fwd-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/_rez_fwd.exe
631 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez.exe
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-benchmark-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-benchmark.exe
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bind-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bind.exe
643 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-build-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-build.exe
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bundle-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-bundle.exe
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-config-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-config.exe
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-context-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-context.exe
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-cp-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-cp.exe
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-depends-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-depends.exe
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-diff-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-diff.exe
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-env-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-env.exe
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-gui-script.pyw
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-gui.exe
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-help-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-help.exe
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-interpret-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-interpret.exe
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-memcache-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-memcache.exe
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-mv-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-mv.exe
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pip-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pip.exe
651 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-cache-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-cache.exe
653 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-ignore-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-pkg-ignore.exe
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-plugins-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-plugins.exe
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-python-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-python.exe
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-release-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-release.exe
637 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-rm-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-rm.exe
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-search-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-search.exe
649 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-selftest-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-selftest.exe
645 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-status-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-status.exe
643 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-suite-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-suite.exe
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-test-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-test.exe
641 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-view-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-view.exe
647 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-yaml2py-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rez-yaml2py.exe
639 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rezolve-script.py
999 2024-05-11 20:21 rez-3.1.1.data/scripts/rez/rezolve.exe
The cool part is that the launchers are only compiled when building a wheel on Windows, and Visual Studio is not even needed! We use the awesome C/C++ compiler provided by the Zig language. Since it's available as an official package on PyPI (ziglang), everything just works.
That's it?
Yes, that's pretty much it. Or almost. We have to talk about wheels a little bit more. Because who doesn't like to talk about wheels?
There is a last detail to talk about and it's the final wheels. We will generate three of them:
- A generic wheel (pure) that works on all platforms. It'll be called
rez-<version>-py3-none-any.whl. - An amd64 wheel for Windows that will be called
rez-<version>-py3-none-win_amd64.whl. - An arm64 wheel for Windows that will be called
rez-<version>-py3-none-win_arm64.whl.
By generating a generic wheel for non-Windows platforms, pip will be able to install rez on everything that is not Windows (macOS, Linux, etc).
And because installers have to prefer more specific wheels before generic ones, they will install the Windows variants if installed on Windows.
@JeanChristopheMorinPerso , might this sort of workflow open the door for us to be able to have rez and rez plugins exist slightly closer together in a standard way? (Thought provoked by TSC meeting)
@maxnbk I'm not sure how to answer this. Can you clarify what you mean by "open the door for us to be able to have rez and rez plugins exist slightly closer together in a standard way" please?
I think I finally got it working with -E. It took some more thinking outside the box, but it now works. See https://github.com/AcademySoftwareFoundation/rez/actions/runs/6278210700/job/17051519463?pr=1536#step:7:74 which proves it. This line comes from a custom command I added to rez, jctest that basically prints sys.flags. In our case, we are interested in sys.flags.ignore_environment which basically proves that -E was passed correctly.
I also adapted the PR so that it works on Linux and macOS too. For that, I had to modify the scripts we generate on unix to
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import re
import sys
import platform
# If -E is not passed, then inject it and re-execute outself.
# Note that this is not done on Windows because the Windows launcher
# already does this.
if not sys.flags.ignore_environment and platform.system() != 'Windows':
args = [sys.executable, '-E'] + sys.argv
if os.getenv('REZ_LAUNCHER_DEBUG'):
print('Launching:', ' '.join(args))
os.execvp(sys.executable, args)
from rez.cli._entry_points import {0}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\\.pyw|\\.exe)?$', '', sys.argv[0])
sys.exit({0}())
This is needed because pip doesn't keep the arguments in the shebang. On Windows, I simply inject -E from the launcher executable. I could have use the same method on unix and Windows, but I know launching processes on Windows is costly and it would have resulted in 3 processes started instead of 2.
Next step, clean up, write a proper workflow to generate the wheels on all platforms, figure out where to store the launcher and write at least one integration test that would do this:
- Install rez with Python version X (say 3.7).
- Create a Python 3.11 rez package (We can take inspiration from what I did in rez-pip).
- Run
rez-env python -- jctest(We should rename it to something like__rez_install_testand make it output a JSON, or something like that). - Check that the output of step 3 is
<rez virtualenv root>/bin/python. - Check that running python results in running Python from the rez package and not the rez install.
I think that this is enough to prove that our install is production ready and works as intended.
TO be even more sure, we could try to install with pip install --target and set PYTHONPATH to point to the rez lib. The rez commands should fail to run because -E prevents PYTHONPATH from being read by Python.
Eventually, we could review the flags used. -E gives us some guarantees, but we can provide even more guarantees by using other flags, like -I (which is is a combination of -E -P -s or we could make it more explicit with -E -s. -P is too new (but would be nice to have... It's only in 3.11). I think we could provide the same flags across all python versions we support).
Umm, it seems like it doesn't work when using pip install --target. But I don't think it's necessarily something we want to support...
I made good progress in the last 3 days. The launchers are now compiled from setup.py (only on Windows). I'm using the Zig compiler to compile the launcher, which means that MSVC isn't used and needed. There is now wheels for Windows x64 and Windows arm64. Other platforms are covered by a pure+universal wheel. I also added an integration test that uses a real rezified python package.
So far everything seems to be working great, except one test on Windows. It looks like rez-gui.exe isn't working. See https://github.com/AcademySoftwareFoundation/rez/actions/runs/6291958825/job/17080881978. It's probably something simple to fix.
There is one major limitation though, and it's that it doesn't support Python 2.
Questions still to be answered (they all have answers and are easy to answer):
- Considering that Linux and macOS use the same wheel and Windows has its own wheels, how will rez-pip react? I'm asking because it's an uncommon situation.
- What should we do with the install script? Should we keep it around? If so, should it use the new install method (it would simply do a
pip install .) or should it still use the old install method (pip install .+ dance for created therezdirectory, etc)? - If we keep the install script, should we also keep the option to install as a rez package?
- Do we want to compile the launchers on every build? It makes things easier, but it's also slower. Now that I use zig to build we can build the launchers on Linux while targeting Windows, so it's much easier to just commit the launchers in binary form.
- Sub-question: Are the launchers reproducible?
- Should we optimize the launchers for size or for performance? The default launchers created by pip are optimized for size. That's also what I did.
- Considering that we have wheels on PyPI already and that right now they will install the CLI tools (even if they shouldn't), should we do a release just to remove the entry points (CLIs)? My concern is that we start to advertise that users can just pip install rez, and that they get an older version of rez in which it wasn't supported and they'd get bugs and problems all over the place...
- Should we include the simple_launcher license in the wheel?
We should maybe try to support relative paths in the python stubs shebang to support cases like https://academysoftwarefdn.slack.com/archives/C0321B828FM/p1695737655931909. Pip would still produce absolute paths in the installed stubs, but at least users could modify them with a post-install script of their own to whip a more portable rez install (even if we don't officially support it).
I think this functionality is already supported by simple_launcher, but I'd have to double check to make sure it is really the case.
Codecov Report
Attention: Patch coverage is 86.56716% with 9 lines in your changes are missing coverage. Please review.
Project coverage is 58.36%. Comparing base (
f63031e) to head (d3940c2). Report is 3 commits behind head on main.
| Files | Patch % | Lines |
|---|---|---|
| src/rez/cli/_entry_points.py | 92.98% | 4 Missing :warning: |
| src/rez/system.py | 50.00% | 3 Missing :warning: |
| src/rez/shells.py | 50.00% | 2 Missing :warning: |
Additional details and impacted files
@@ Coverage Diff @@
## main #1536 +/- ##
==========================================
+ Coverage 58.30% 58.36% +0.06%
==========================================
Files 126 126
Lines 17159 17142 -17
Branches 3505 3506 +1
==========================================
+ Hits 10004 10005 +1
+ Misses 6492 6475 -17
+ Partials 663 662 -1
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.