Skip to content

fix: use sys.executable for pip and python subprocesses to support venv environments#503

Merged
danielhanchen merged 3 commits into
unslothai:mainfrom
rolandtannous:fix/use-sys-executable-for-pip-and-python
Feb 17, 2026
Merged

fix: use sys.executable for pip and python subprocesses to support venv environments#503
danielhanchen merged 3 commits into
unslothai:mainfrom
rolandtannous:fix/use-sys-executable-for-pip-and-python

Conversation

@rolandtannous

Copy link
Copy Markdown

Problem

When running from a Python virtual environment (.venv, conda, poetry, etc.), GGUF conversion fails because llama_cpp.py shells out to bare pip and python commands, which resolve to the system Python — not the interpreter running the current process.

This causes two distinct failures:

1. pip install hits PEP 668 protection (exit code 1)

check_pip() iterates ["uv pip", "pip", "pip3", ...] and finds the system's pip, which on modern Debian/Ubuntu (PEP 668) refuses to install packages outside a virtual environment:

RuntimeError: Unsloth: GGUF conversion failed: [FAIL] Command `pip install gguf protobuf sentencepiece mistral_common` failed with exit code 1
stdout: error: externally-managed-environment

2. python command not found during GGUF conversion (exit code 127)

The conversion command is hardcoded as:

command = f"python {converter_location} {args_str} {input_folder}"

On many Linux distributions, there is no bare python binary — only python3. Even where python exists, it points to the system Python, not the venv interpreter, so it would lack the required packages.

subprocess.CalledProcessError: Command 'python llama.cpp/unsloth_convert_hf_to_gguf.py ...' returned non-zero exit status 127.

3. uv pip fails in non-uv environments (exit code 2)

On systems where uv is installed globally but the Python environment is managed by conda, mamba, or a standard venv (not uv), and after #336 was merged, check_pip() finds uv pip first (it was the first entry in PIP_OPTIONS) and tries to use it. But uv pip install requires a uv-managed venv and fails:

RuntimeError: [FAIL] Command `uv pip install gguf protobuf sentencepiece mistral_common` failed with exit code 2
stdout: error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment

By placing sys.executable -m pip before uv pip in PIP_OPTIONS, check_pip() now finds a working pip before ever reaching the uv pip entry, resolving this issue as well.

Root Cause

All three issues stem from the same root cause: subprocess calls resolve to system-wide executables instead of the currently-running Python interpreter's environment.

Fix

Replace bare pip/python references with sys.executable, which always resolves to the Python binary running the current process. This is the officially recommended way to invoke pip programmatically.

Change 1: PIP_OPTIONS (line 70)

Prepend f"{sys.executable} -m pip" so that check_pip() prefers the current interpreter's pip:

 PIP_OPTIONS = [
+    f"{sys.executable} -m pip",  # Always prefer the running interpreter's pip
     "uv pip",
     "pip",
     "pip3",
     "python3 -m pip",
     "py -m pip",
     "poetry",
 ]

Change 2: GGUF conversion command (line 1030)

Use sys.executable instead of bare python:

- command = f"python {converter_location} {args_str} {input_folder}"
+ command = f"{sys.executable} {converter_location} {args_str} {input_folder}"

Why sys.executable?

sys.executable returns the absolute path of the Python interpreter running the current process. It correctly resolves in all common environments:

Environment sys.executable resolves to Works?
Standard venv /path/to/.venv/bin/python
Conda / Mamba /path/to/conda/envs/myenv/bin/python
uv /path/to/.venv/bin/python
Poetry /path/to/.venv/bin/python
System Python (Colab/Kaggle) /usr/bin/python3

For check_pip() specifically, the fallback list still contains "uv pip", "pip", etc., so environments where sys.executable -m pip doesn't work (e.g., uv environments that don't ship pip) gracefully fall through to the next option.

Alternatives Considered

Approach Verdict
sys.executable -m pip / sys.executable Chosen. Standard practice, minimal change, works everywhere
Modify PATH/VIRTUAL_ENV env vars before subprocess calls ❌ Fragile, assumes venv layout, requires threading env through many functions
UNSLOTH_STUDIO=1 env var to switch behavior ❌ Adds coupling, doesn't fix root cause, easy to forget
--break-system-packages flag ❌ Explicitly discouraged by PEP 668, can corrupt system Python

Testing

  • Verified GGUF export completes successfully from a .venv environment on a PEP 668 system (Ubuntu with Python 3.12)
  • pip packages install into the correct venv
  • convert_hf_to_gguf.py runs under the venv Python with access to all required packages
  • No behavioral change for non-venv environments (Colab, Kaggle, bare system Python)

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @rolandtannous, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and compatibility of GGUF conversion processes across various Python virtual environments. By ensuring that subprocess calls correctly target the active Python interpreter and its associated tools, it eliminates common installation and execution failures, leading to a more reliable user experience regardless of the environment setup.

Highlights

  • Virtual Environment Compatibility: Resolved issues where GGUF conversion failed in virtual environments (e.g., .venv, conda, poetry) due to subprocess calls resolving to system-wide Python or pip executables instead of the current environment's interpreter.
  • PEP 668 Protection Bypass: Addressed pip install failures caused by PEP 668 protection on modern Debian/Ubuntu systems, which prevents installing packages outside a virtual environment when using the system's pip.
  • Python Command Resolution: Fixed 'python command not found' errors during GGUF conversion by ensuring the correct Python interpreter from the virtual environment is used, even when a bare python binary is absent or points to the system Python.
  • Improved uv pip Handling: Prevented uv pip failures in non-uv managed environments by prioritizing sys.executable -m pip in the PIP_OPTIONS list, allowing check_pip() to find a working pip before attempting uv pip.
  • Standardized Subprocess Invocation: Implemented the officially recommended method of invoking pip and Python programmatically by replacing bare pip and python references with sys.executable -m pip and sys.executable respectively.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • unsloth_zoo/llama_cpp.py
    • Modified the PIP_OPTIONS list to prepend f"{sys.executable} -m pip", ensuring the current interpreter's pip is prioritized for package installations.
    • Updated the GGUF conversion command to use sys.executable instead of a bare python command, guaranteeing the correct Python interpreter is used for the conversion script.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0652b78787

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/llama_cpp.py Outdated
]

PIP_OPTIONS = [
f"{sys.executable} -m pip", # Always prefer the running interpreter's pip

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate pip candidate before preferring sys.executable

Prepending f"{sys.executable} -m pip" to PIP_OPTIONS causes check_pip() to select that entry even when it cannot actually install packages (for example, interpreters where the pip module is absent), because check_pip() only filters on command-not-found text and does not check return codes. In this case try_execute(f"{pip} install ...") fails immediately and the code never reaches fallback options such as uv pip or pip3, so GGUF dependency installation regresses in those environments.

Useful? React with 👍 / 👎.

@rolandtannous rolandtannous Feb 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@codex review

  • pip must exist in the current interpreter — the package was installed with it (pip install unsloth-zoo unsloth)
  • The concern is pre-existing — check_pip() has the same flaw for uv pip and every other entry already but check the previous point.
  • sys.executable -m pip is actually the safest entry in the list because it's the only one guaranteed to target the correct environment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@codex review

Comment thread unsloth_zoo/llama_cpp.py Outdated
if print_output: print(f"\nUnsloth: Converting {description}...")
args_str = " ".join(f"{k} {v}" for k, v in args.items())
command = f"python {converter_location} {args_str} {input_folder}"
command = f"{sys.executable} {converter_location} {args_str} {input_folder}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Quote interpreter path in conversion shell command

The updated conversion command inserts sys.executable into a shell=True command string without quoting, so interpreter paths containing spaces (such as Windows installs under Program Files or workspace paths with spaces) are tokenized incorrectly and fail before running the converter. This makes GGUF conversion fail in those path layouts after this change.

Useful? React with 👍 / 👎.

@rolandtannous rolandtannous Feb 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updated to
command = f'"{sys.executable}" {converter_location} {args_str} {input_folder}'
should be enough @codex review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@codex review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request correctly fixes an issue with using the wrong Python interpreter in virtual environments by using sys.executable. The changes are a definite improvement. However, I've identified a critical command injection vulnerability related to the way subprocess commands are constructed and executed with shell=True. While this is a pre-existing issue, the modified code is part of the vulnerable section. I've added a comment with details on the vulnerability and a recommendation for a fix, even though a fully applicable code suggestion is not possible due to repository API limitations.

Comment thread unsloth_zoo/llama_cpp.py Outdated
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Feb 17, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5405fc8bff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_zoo/llama_cpp.py Outdated
]

PIP_OPTIONS = [
f"{sys.executable} -m pip", # Always prefer the running interpreter's pip

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip sys.executable pip when pip module is unavailable

Adding f"{sys.executable} -m pip" as the first PIP_OPTIONS entry regresses uv-created environments that don’t seed pip by default (see uv venv --help: --seed is required to install pip). In check_pip(), availability is inferred only from “command not found” text and ignores non-zero exit codes, so python -m pip is treated as valid even when it fails with No module named pip; the later install then aborts and never falls through to uv pip, breaking GGUF setup for those users.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

pip must exist in the current interpreter — the main package was installed with it (pip install unsloth-zoo unsloth)
The concern is pre-existing — check_pip() has the same flaw for uv pip and every other entry already but check the previous point.
sys.executable -m pip is actually the safest entry in the list because it's the only one guaranteed to target the correct environment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5405fc8bff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth_zoo/llama_cpp.py Outdated
]

PIP_OPTIONS = [
f"{sys.executable} -m pip", # Always prefer the running interpreter's pip

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip broken pip candidate before returning from check_pip

Placing f"{sys.executable} -m pip" first causes a regression in environments where the running interpreter has no pip module (for example, unseeded uv venvs): check_pip() only treats command not found output as failure and does not check the subprocess exit code, so it returns this candidate even when it fails with No module named pip. The later install command then fails immediately instead of falling back to uv pip/pip entries, so GGUF conversion can no longer proceed in those environments.

Useful? React with 👍 / 👎.

@danielhanchen

Copy link
Copy Markdown
Member

Pushed follow-up fixes to this PR branch in commit 06f68e3.

What I changed

  • Hardened check_pip() candidate selection in unsloth_zoo/llama_cpp.py:
    • Added safer candidate validation (shlex parsing and metachar rejection).
    • Added missing-pip-module detection.
    • Kept quoted interpreter candidate ("{sys.executable}" -m pip) for space-safe shell probing.
    • Used command probes that reduce false positives (uv pip --help, others --version).
  • Fixed GGUF converter invocation robustness:
    • Switched conversion execution to argv with shell=False.
    • Kept flag-only options (notably --mmproj) as standalone args.
    • Improved conversion failure error context.
  • Fixed edge-case crash when both supported arch sets are None.

Validation

  • PYTHONDONTWRITEBYTECODE=1 python -m py_compile unsloth_zoo/llama_cpp.py
  • Consolidated stress harness run (local): ALL_OK=1
    • rejects false-positive pip candidates
    • keeps uv pip fallback behavior
    • verifies shell=False conversion path
    • verifies --mmproj flag-only behavior
    • verifies no injection behavior with metacharacter paths
    • verifies no crash when both supported arch sets are None

@danielhanchen

Copy link
Copy Markdown
Member

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This is an excellent pull request that addresses a critical issue with virtual environment support. The detailed problem description and analysis are very helpful. The changes are well-implemented, not only fixing the immediate problem by using sys.executable but also significantly improving the robustness and security of the subprocess calls. The rewrite of check_pip is much more reliable, and the switch to shell=False in convert_to_gguf is a great security enhancement. I have one minor suggestion to further improve the code's readability and performance.

Comment thread unsloth_zoo/llama_cpp.py
tokens = shlex.split(pip)
except ValueError:
return False
if tokens in (["pip"], ["pip3"], ["uv", "pip"], ["poetry"]):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For better performance and readability, consider using a set of tuples for the known commands. This allows for O(1) average time complexity for lookups, which is more efficient than the current O(n) for a tuple of lists. It's a more idiomatic way to check for membership in a collection of fixed items.

Suggested change
if tokens in (["pip"], ["pip3"], ["uv", "pip"], ["poetry"]):
if tuple(tokens) in {("pip",), ("pip3",), ("uv", "pip"), ("poetry",)}:

@danielhanchen danielhanchen merged commit 6b68187 into unslothai:main Feb 17, 2026
danielhanchen added a commit to shimmyshimmer/unsloth-zoo-staging-2 that referenced this pull request May 5, 2026
- convert_to_gguf builds an env that prepends UNSLOTH_LLAMA_CPP_SCRIPTS_DIR/gguf-py to PYTHONPATH (when that directory exists) and passes it via env= to both subprocess.run call sites. Without this, the parent process's sys.path mods made by use_local_gguf do not propagate into the converter's child interpreter, so a user that pinned a newer convert_hf_to_gguf.py via the env var ended up running it against the default-install or system-installed gguf and saw ModuleNotFoundError or version-mismatch failures.
- use_local_gguf now also emits a logger.warning when UNSLOTH_LLAMA_CPP_SCRIPTS_DIR is set but the directory has no gguf-py/ subdirectory, mirroring the warning pattern in _resolve_local_convert_script. This surfaces the cross-version mismatch case that previously silently fell back to LLAMA_CPP_DEFAULT_DIR.
- _resolve_local_convert_script's missing-converter warning now mentions both accepted filenames (convert_hf_to_gguf.py and convert-hf-to-gguf.py) so users debugging the env var see the full set the resolver actually probes.
- Bump @lru_cache(2) on _download_convert_hf_to_gguf_cached to maxsize=8 so cross-mode toggling between local and network sources, plus a few in-place updates of a pinned converter, no longer evict cache slots and trigger redundant downloads or re-reads. Negligible memory overhead because the cached payload is small.
- Reorder check_llama_cpp's fallback list to ["convert_hf_to_gguf.py", "convert-hf-to-gguf.py"] so it matches _resolve_local_convert_script's underscore-first preference; modern llama.cpp ships only the underscore variant, so this also avoids an unnecessary stat on the legacy hyphenated name in the common case.

Blame-touched lines preserved in intent:
- The print-output `subprocess.run(command, shell=False, check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)` continuation line (line 1443, which carries the stdout=subprocess.PIPE, stderr=subprocess.STDOUT keyword arguments) whose blame points at 70f2ce7 ("[Part 1] Complete llama.cpp Integration Overhaul ... Multi-Modal Support (unslothai#302)") still passes the same shell=False/check=True/text=True/stdout=subprocess.PIPE/stderr=subprocess.STDOUT arguments in the same order; only `env=sub_env` is added at the end so the venv-friendly invocation also picks up the pinned gguf-py via PYTHONPATH instead of the default-install gguf.
- The silent-output `subprocess.run(command, shell=False, check=True, capture_output=True)` line (line 1446) whose blame points at 6b68187 ("fix: use `sys.executable` for pip and python subprocesses to support venv environments (unslothai#503)") still passes the same command/shell=False/check=True/capture_output=True; only `env=sub_env` is added so the same sys.executable launch the original fix established also receives PYTHONPATH for the pinned gguf-py.
- The `try`/`except subprocess.CalledProcessError` block surrounding both calls (whose blame points at 70f2ce7 "[Part 1] Complete llama.cpp Integration Overhaul ... Multi-Modal Support (unslothai#302)" and 71229d8 "Fix Mistral, Qwen (unslothai#36)") is unchanged: same RuntimeError type, same command-string formatting, same stdout-print-on-failure path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants