Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Python venv creation #326094

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft

Fix Python venv creation #326094

wants to merge 2 commits into from

Conversation

mcdonc
Copy link
Contributor

@mcdonc mcdonc commented Jul 10, 2024

This is a reapplication of commits by @cwp from #297628, which aimed to fix Python virtualhost creation on top of a newer master. The original PR was closed after it caused regressions. The related Discourse discussion is at https://discourse.nixos.org/t/how-to-build-python-virtualenv-with-packages-provided-by-python3-withpackages/24766

https://hydra.nixos.org/jobset/nixpkgs/python-env-venv#tabs-evaluations

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/how-to-build-python-virtualenv-with-packages-provided-by-python3-withpackages/24766/27

makeWrapper "$path/bin/$prg" "$out/bin/$prg" --set NIX_PYTHONPREFIX "$out" --set NIX_PYTHONEXECUTABLE ${pythonExecutable} --set NIX_PYTHONPATH ${pythonPath} ${lib.optionalString (!permitUserSite) ''--set PYTHONNOUSERSITE "true"''} ${lib.concatStringsSep " " makeWrapperArgs}
if [ -f ".$prg-wrapped" ]; then
echo "#!${pythonExecutable}" > "$out/bin/$prg"
sed -e '1d' -e '3d' ".$prg-wrapped" >> "$out/bin/$prg"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason this original PR got reverted, was the assumption that you can undo every wrapper by deleting those lines which sometimes deleted legit python code like for pretalx.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it still deletes part of pretalx-manage:

#!/nix/store/pc8n829fmq2494s5vadw7cppiwfn68a7-python3-3.12.4-env/bin/python3.12
import sys;import site;import functools;sys.argv[0] = '/nix/store/07rcj9vcgvmsq7aqgi39csw7ypsyv990-pretalx-2024.1.0/bin/pretalx-manage';functools.reduce(lambda k, p: site.addsitedir(p, k), [';
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretalx.settings")

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

vs https://github.com/pretalx/pretalx/blob/v2024.1.0/src/manage.py

missing the os import.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RuRo suggests a somewhat hardened sed expression in https://discourse.nixos.org/t/how-to-build-python-virtualenv-with-packages-provided-by-python3-withpackages/24766/20

sed \
    -e '/^#!\/nix\/store\//d' \
    -e '/^import sys;import site;import functools;sys\.argv\[0\] = /d' \
    ".$prg-wrapped" >> "$out/bin/$prg"

Comment on lines -4 to -5
The module recursively adds paths that are on `NIX_PYTHONPATH` to `sys.path`. In
order to process possible `.pth` files `site.addsitedir` is used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does dropping sitecustomize.py mean that packages whose correct behavior relies on .pth files (like setuptools as discovered in #326321 (comment)) would be broken? Or have I overthought this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that the logic in sitecustomize.py was essentially manually emulating what normally happens with the correct base_prefix set, including the .pth logic.

Assuming this PR works as intended, then the base_prefix would be set to the output path of the -env python derivation and so the ${sys.base_prefix}/lib/python3.X/site-packages directory (that includes all the required package files) would automatically be considered a site directory (and thus all the .pth files inside it would be executed when they are supposed to).

@@ -566,8 +566,7 @@ in with passthru; stdenv.mkDerivation (finalAttrs: {
# Strip tests
rm -R $out/lib/python*/test $out/lib/python*/**/test{,s}
'' + optionalString includeSiteCustomize ''
# Include a sitecustomize.py file
cp ${../sitecustomize.py} $out/${sitePackages}/sitecustomize.py

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an empty string left here, I think that's not needed?

is_nixenv = "False";
is_virtualenv = "False";
};
} // lib.optionalAttrs (python.pythonAtLeast "3.8" && (!python.isPyPy)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that you repeat line 41 here? Could be one block

Also a comment why it needs to be at least python 3.8 would be nice.
The current comments around pypy executables is already quite confusing when it's in a block that doesn't support pypy anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for python 3.8 is probably because python 2 doesn't support venv. Quoting from virtualenvs website:

Since Python 3.3, a subset of it has been integrated into the standard library under the venv module.

is_nixenv = "True";
is_virtualenv = "True";
};
} // lib.optionalAttrs (!python.isPyPy) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that you repeat line 41 here? Could be one block

@@ -35,14 +35,22 @@ let
fi
mkdir -p "$out/bin"

rm -f $out/bin/.*-wrapped
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that every wrapped executable in all of paths are python scripts? Otherwise this seems risky to me, but i might be missing something?

A short comment explaining why this is needed and safe would be appreciated. Is it just for the check below?

@yajo
Copy link
Contributor

yajo commented Aug 2, 2024

You might want to consider this one-line alternative: #261801

@RuRo
Copy link
Contributor

RuRo commented Aug 3, 2024

You might want to consider this one-line alternative: #261801

FYI, as far as I can tell, that PR doesn't actually fix the problem, but just adds a nix-specific way to work around the problem. With that PR, nested venvs would still be broken, but you could achieve a similar result by manually adding stuff to NIX_PYTHONPATH.

So properly fixing venvs is still probably desirable.

@yajo
Copy link
Contributor

yajo commented Aug 8, 2024

Yes, indeed that's what it does. And of course a proper fix is better.

But I think #261801 is still valuable because:

  • it works
  • it's 1 line of fix workaround
  • it's available today (actually, since October 2023!)
  • doesn't harm
  • it could even be backported to stable, as a mostly-backwards-compatible fix.

This one PR is still draft after 1 month. I'm afraid it could become stale, or reach a dead end just like #302385 did.

And if it doesn't... perfect! Just remove my workaround once it works. Still no reason to ignore #261801 IMHO 😊

@SuperSandro2000
Copy link
Member

  • doesn't harm

well, I am not sure. Right now we are always overwriting the variable and nesting could surely trigger some edge cases.

@phaer
Copy link
Member

phaer commented Aug 8, 2024

Sorry, missclicked on mobile

@phaer phaer closed this Aug 8, 2024
@phaer phaer reopened this Aug 8, 2024
@wegank wegank added the 2.status: merge conflict This PR has merge conflicts with the target branch label Sep 27, 2024
@vlaci
Copy link
Contributor

vlaci commented Sep 27, 2024

I also tried to take a stab at this.

It looks like it is a hell of a job to develop, as experimenting with the simplest Python derivation results in mass rebuilds. My idea to circumvent this while testing different approaches is to copy the related wrapping code, and wire up a new python derivation to stop cascading builds and also have some output that I can experiment with.

IDK if it is a viable approach.

@RuRo
Copy link
Contributor

RuRo commented Sep 27, 2024

@vlaci my understanding is that the hardened sed expression I suggested is still a suboptimal solution, and the proper solution should avoid adding those magic lines in the first place. I'd recommend you to try contacting @cwp directly about this, since they previously mentioned that they have an idea about how to best implement this.

But I am totally in agreement with you, that fixing this issue is a pain due to the need to rebuild python itself and everything python-related from source. Also, the fact that the previous PR was reverted due to regressions indicates that python packages might not have sufficient integration tests, so it's borderline impossible to get this right without breaking things.

cwp added 2 commits October 19, 2024 15:12
The way we build python environments is subtly broken. A python
environment should be semantically identical to a vanilla Python
installation in, say, /usr/local. The current implementation, however,
differs in two important ways. The first is that it's impossible to use
python packages from the environment in python virtual environments. The
second is that the nix-generated environment appears to be a venv, but
it's not.

This commit changes the way python environments are built:

  * When generating wrappers for python executables, we inherit argv[0]
    from the wrapper. This causes python to initialize its configuration
    in the environment with all the correct paths.
  * We remove the sitecustomize.py file from the base python package.
    This file was used tweak the python configuration after it was
    incorrectly initialized. That's no longer necessary.

The end result is that python environments no longer appear to be venvs,
and behave more like a vanilla python installation. In addition it's
possible to create a venv using an environment and use packages from
both the environment and the venv.

(cherry picked from commit 234bb31)
When building a python environment's bin directory, we now detect
wrapped python scripts from installed packages, and generate unwrapped
copies with the environment's python executable as the interpreter.

(cherry picked from commit 9611885)
@ofborg ofborg bot removed the 2.status: merge conflict This PR has merge conflicts with the target branch label Oct 19, 2024
@ofborg ofborg bot requested review from mweinelt and natsukium October 19, 2024 22:49
@MagicRB
Copy link
Contributor

MagicRB commented Nov 23, 2024

I've taken a stab at this, it seems like the core issue is that the preamble is executed when it shouldn't be. I therefore propose this slightly altered preamble:

''
  import sys
  import site
  import functools
  _nix_wrapped = '"'$f'"'.split('"'/'"')
  _nix_direct = sys.argv[0] == '"'/'"'.join([*_nix_wrapped[:-1], '"'''"'.join(['"'.'"', _nix_wrapped[-1], '"'-wrapped'"']) ])
  sys.argv[0] = '"'$(readlink -f "$f")'"' if _nix_direct else sys.argv[0]
  functools.reduce(lambda k, p: site.addsitedir(p, k), ['"$([ -n "$program_PYTHONPATH" ] && (echo "'$program_PYTHONPATH'" | sed "s|:|','|g") || true)"'], site._init_pathinfo()) if _nix_direct else None
''

This prevents the execution of the preamble when it shouldn't execute, which means we don't need to edit the script at all.

@MagicRB
Copy link
Contributor

MagicRB commented Nov 24, 2024

Okay different idea, what if wrap-python.nix creates a nix-support/site-packages/<python-version>/... directory with symlinks to what would be hardcoded in the executable. There would still be a preamble but it would just add that directory using addsitedir which would simplify things a lot. Then if you move the executable you unlink it from its dependencies, which once again solves the problem of having to delete the preamble when building a venv. I think this approach is cleaner in general but would add a directory with symlinks to every single store path which is wrapped by wrap-python.nix. It would also need changes in python3.withPackages to accommodate the need to merge that. Or we could rely on nix-support/propagated-build-inputs. What do people think? I'll implement some of these this evening to actually test whether they work.

@MagicRB MagicRB mentioned this pull request Nov 24, 2024
@MagicRB
Copy link
Contributor

MagicRB commented Nov 24, 2024

I ended up implementing a completely different approach, which works, its not pretty, but hey it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.