Studio: apply base.txt on the install.sh and install.ps1 paths (#8195)

* Studio: apply base.txt on the install.sh and install.ps1 paths

install.sh and install.ps1 install unsloth and unsloth-zoo inline, then
export SKIP_STUDIO_BASE=1 so setup.sh / setup.ps1 do not install the same
two packages a second time. install_python_stack.py read that flag as
"skip base.txt" and short-circuited the whole step:

    if skip_base:
        pass

That was the same thing only for as long as base.txt held nothing but
those two names. Add a third, pinned entry to base.txt and it reaches no
fresh install on any platform: neither installer reads the file, and the
one branch that does was skipped. It would only land later, if the user
happened to run `unsloth studio update`.

Every install.sh and install.ps1 path was affected, on every platform:
CUDA, ROCm, XPU, CPU, macOS, local and non-local, fresh and migrated.

Keep skipping the two core packages, which is all the flag was ever
meant to avoid repeating, and apply whatever else base.txt asks for.
When base.txt holds only the core packages, as it does today, there is
nothing left to install and no extra subprocess runs. No-torch mode is
untouched: it has its own list in no-torch-runtime.txt, which the
installers do apply inline.

The core-package filter parses the project name rather than matching on
a prefix, so a future unsloth-<something> pin is not swallowed too.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Reconcile base requirements with current main

* Preserve relative requirements includes across filters

* Fix filtered requirements test cleanup

* Separate core and shared base requirements

* Preserve shared base requirement resolution

* Keep the filtered-requirements and uv alias paths from aborting an install

The adjacent temp copy raised PermissionError on a read-only requirements dir, and a symlink failure handed uv back the spaced path it cannot read. Fall back to the temp dir and to a copy respectively, and stop the real-extras tests leaving filtered files in the tree.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Count the MLX slot and survive an unusable base.txt

Simulating every install path showed two gaps. base_total never counted the Apple Silicon MLX step, so `studio update` there ran 13 steps out of a declared 12 and recorded the wrong steps_total. And the new base.txt read happens before the manifest is dropped, so a missing or unreadable file aborted with a traceback where the old code reached pip; a BOM also read as content and scheduled an empty step. Progress coverage now spans both core paths on all four platforms.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the unreadable base.txt case independent of the mode bits

chmod(0o000) denies nothing as root, which containerized test jobs run as, and Windows does not implement POSIX modes at all, so the case asserted None against a file it could still read. Raise from a patched read instead.

* Tighten the comments this PR adds

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
This commit is contained in:
Daniel Han 2026-08-11 00:06:23 -07:00 committed by GitHub
parent 0e488c211b
commit 3a58fa5c41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 410 additions and 88 deletions

View file

@ -4,13 +4,11 @@ from __future__ import annotations
import ast
import contextlib
import glob
import importlib
import io
import os
import re
import sys
import tempfile
from pathlib import Path
from unittest import mock
@ -86,16 +84,18 @@ class TestUvSafePath:
assert ips._uv_safe_path(p) == p
@pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback")
def test_posix_space_path_returns_spacefree_copy(self, tmp_path):
def test_posix_space_path_preserves_relative_requirements(self, tmp_path):
src = tmp_path / "Open Source" / "constraints.txt"
src.parent.mkdir(parents = True)
src.write_text("torch>=2.6\n")
src.write_text("-r child.txt\n")
(src.parent / "child.txt").write_text("torch>=2.6\n")
out = ips._uv_safe_path(str(src))
assert " " not in out, f"uv-safe path still has a space: {out!r}"
assert out != str(src)
assert Path(out).read_text() == "torch>=2.6\n"
assert Path(out).read_text() == "-r child.txt\n"
assert (Path(out).parent / "child.txt").read_text() == "torch>=2.6\n"
@pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback")
def test_posix_missing_file_falls_back_to_original(self):
@ -121,24 +121,23 @@ class TestUvSafePathHardening:
assert uvps.uv_safe_path(str(src)) == str(src)
@pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback")
def test_no_temp_dir_leak_on_copy_failure(self, tmp_path, monkeypatch):
"""A copyfile failure after mkdtemp must not orphan the temp dir."""
def test_alias_failure_falls_back_to_a_copy(self, tmp_path, monkeypatch):
"""A symlink failure must still hand uv a space-free path, and not orphan the dir."""
from backend.utils import uv_path_safety as uvps
src = tmp_path / "Open Source" / "constraints.txt"
src.parent.mkdir(parents = True)
src.write_text("idna\n")
pattern = os.path.join(tempfile.gettempdir(), "unsloth_uv_*")
before = set(glob.glob(pattern))
def boom(*a, **k):
raise OSError("boom")
monkeypatch.setattr(uvps.shutil, "copyfile", boom)
monkeypatch.setattr(uvps.os, "symlink", boom)
out = uvps.uv_safe_path(str(src))
assert out == str(src)
assert set(glob.glob(pattern)) == before
assert " " not in out
assert Path(out).read_text() == "idna\n"
assert str(Path(out).parent) in uvps._UV_SAFE_PATH_TMPDIRS
@pytest.mark.skipif(ips.IS_WINDOWS, reason = "POSIX temp-copy fallback")
def test_cleanup_removes_and_clears_registry(self, tmp_path):
@ -149,7 +148,7 @@ class TestUvSafePathHardening:
src.parent.mkdir(parents = True)
src.write_text("idna\n")
out = uvps.uv_safe_path(str(src))
tmp_dir = Path(out).parent
tmp_dir = Path(out).parents[1]
assert tmp_dir.is_dir() and str(tmp_dir) in uvps._UV_SAFE_PATH_TMPDIRS
uvps._cleanup_uv_safe_path_tmpdirs()
@ -516,9 +515,9 @@ class TestProgressLineNotes:
class TestBuildPipCmdUpgradeIntent:
"""pip has no --upgrade-package, so uv's flag must be translated, not dropped.
Dropping it made the fallback a no-op on the update path: `studio update`
passes --upgrade-package unsloth with a base.txt listing a bare unsloth, so
pip found it satisfied, installed nothing, and the update reported success.
Dropping it made the fallback a no-op on the update path: pip saw the named
distributions as already satisfied, installed nothing, and the update
reported success.
"""
def test_update_path_keeps_the_upgrade_intent(self):