| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
desktop: add interface scaling setting (#9666) * desktop: add interface scaling control * desktop: fix interface scale edge cases * desktop: fix native zoom interactions * desktop: serialize interface scale updates * studio: move font and scale settings first * studio: bind the mac chrome constants, floor the scale at 50%, bound first paint Review follow-ups. The floor moves 25 -> 50. Chrome and VS Code allow 25% but both ship Cmd/Ctrl+0, and there is no reset accelerator here or on main, so at 0.25 the Settings row you would use to undo it renders around 3.5px and the value is device-local in localStorage. A comment says to drop it back to 25 once the accelerator lands. 34 and 78 existed in three places with nothing binding them: interface-scale-runtime.ts divided by the literals, provider.tsx repeated them as CSS fallbacks, and interface-scale.test.ts hardcoded 34 / 0.35. The runtime now exports both the numbers and the var(...) strings built from them, provider.tsx imports those, and the test asserts the fallback is built from the constant rather than retyped. Changing the titlebar height in one place can no longer leave a stale divisor at any scale but 100%. applyInterfaceScale is now wrapped for startup. It was the only thing gating first paint on the Tauri IPC bridge, and a rejection was caught while a hang was not, so a wedged bridge meant a permanently blank window rather than a slow one. Past a 1s deadline the app renders at 100% and the effect in provider.tsx applies the real scale when the bridge answers. Two tests, one with a setZoom that never resolves. The comment on nativeDropPointToCss lost ScreenToClient device pixels, NSView points and widget coordinates in the rewrite. Those are why the two branches are allowed to differ, so they are back, with the zoom caveat appended and a note not to collapse both paths onto devicePixelRatio. Same for the deleted regression note on native-training-dataset-drop.test.ts:114. The new drop tests all passed the zoom explicitly, so the default argument, which is the seam between the scale store and the drop path, never ran. One case now omits it. 4826 frontend tests pass, typecheck, build, i18n:check:strict clean, and provider.tsx gains no new eslint findings. * tests: read the mac chrome contract through the interface-scale constants Repo tests (CPU) has been red on this branch since f7bea0d8c5 and the log had expired, so nobody had seen why. Two failures, both the same cause: test_desktop_reliability_frontend_contract.py reads px values straight out of provider.tsx, and the calc() rewrite replaced the literals it greps for. test_collapsed_tauri_keeps_history_arrows_and_adds_new_chat_by_model_picker asserted the literal string '"--studio-collapsed-chat-controls-inset": "188px"'. It now resolves the block and asserts the number is 188, scoped to the mac block because the custom titlebar sets the same var to 12px. test_tauri_collapse_removes_the_icon_rail_but_web_keeps_it went through _chrome_style_blocks, whose regex only captured "--var": "literal" pairs, so --studio-desktop-titlebar-height read as absent once it became an identifier and the offset + button <= titlebar assertion had nothing to compare. The helper now also takes templates and identifiers, resolving the latter out of interface-scale-runtime.ts so the 34 stays in one place. _px understands var(--x, Npx) and calc(Apx + var(--x, Bpx)) as their 100% values, with a docstring saying that is the only scale a static read can see. Formatting via scripts/run_ruff_format.py. 36 of 36 in that file pass locally. * studio: release the interface-scale queue when first paint times out Codex P2 on bebea8d15f. applyInterfaceScaleBeforeFirstPaint's deadline only resolved its own promise, so a setZoom that never settles left interfaceScaleApplicationQueue chained to a permanently pending application. The app rendered, but the provider retry and every later user scale change queued behind that entry and never ran, so scaling stayed dead until restart. The timeout now resets the queue as well as unblocking the render. A late completion from the abandoned call can still arrive after a newer scale has been applied, so the commit is guarded on the scale it read still being the one being asked for; otherwise it would report a stale zoom as live. Two tests, both timed. Without the release they hang rather than fail, which wedges the runner instead of reporting, and that is worth not shipping. Also renames scrollRowPadding to unrailedRowPadding in the alignment test. Main renamed it in #9918 while this branch still used the old name, so the merge took main's source and left our test grepping for a string that no longer exists. That is what broke Frontend build + bundle sanity and Frontend unit tests (Windows); both were green before main moved. Merged upstream/main (411b40615a). tsc clean, 5863 of 5866 frontend tests pass locally, the 3 failures are markdown/Streamdown and are green in CI. * desktop: restore the latest zoom and parse complete scale values * preserve mac window button clearance in mobile navbar * keep window constraints in sync with interface zoom --------- Co-authored-by: LeoBorcherding <borchborchmail@gmail.com> Co-authored-by: Etherl <61019402+Etherll@users.noreply.github.com> | 19 天前 | |
Start the bottom taper under the icon so the disc still reads round (#8321) Easing from the icon centre line took the halo down to 75 percent of its sideways value at 40pt and 47 percent at 60pt, which is inside the disc a viewer actually sees, so the glow read as flattened along the bottom. The taper now waits until 50pt, inside the icon's own lower half, and runs over 30pt: 99 percent of sideways at 40pt, 80 percent at 60pt, and the icon label sits on 9.3 percent mean tint against 10.0 before. Regenerated on macOS with tiffutil, so the asset keeps the same two-page structure and sRGB profile. | 1 个月前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
studio: add in-app updates for debian installs (#11338) * studio: add authenticated debian desktop updates * desktop: publish signed debian updater assets * desktop: stop telling debian users to update by hand The release notes are published twice: as the GitHub release body and as the notes field of latest.json, which the app itself renders. They still said Linux in-app updates are AppImage-oriented and that package installs should update by downloading a new package, which this branch makes false for .deb. A .deb installed before in-app updates shipped still needs one manual update, because it has no updater entry to find, so say that rather than dropping the caveat entirely. The two comments in use-tauri-update.ts justified the give-up branch with "latest.json has no deb/rpm key". It has linux-x86_64-deb now. The branch is still right for rpm, a plain tarball, and a .deb whose updater checks fail, so the code is unchanged and only the reason is restated. * desktop: assert the polkit action actually ships in the deb The whole Debian update path hangs on one file reaching /usr/share/polkit-1/actions/, and it gets there through a bundle.linux.deb.files mapping in tauri.conf.json that nothing validated. Drop that entry and is_supported_install() keeps returning true, pkexec finds no action for the binary, and every update falls back to the release page with no test failing anywhere. The analogous AppImage files map is already pinned by test_release_desktop_appimage.py, so this closes the gap on the deb side. The clean machine lane already installs the .deb and runs dpkg -L, so the assertion goes there: the action is shipped, it is root:root 0644, and all three implicit cases still require auth_admin. Checked against a package built from this branch, and against packages with the entry dropped, with auth_admin weakened to yes, with one case left behind, world-writable, and owned by a non-root user, each of which fails it. The branding contract listed every release asset suffix except Ubuntu.deb.sig, which this branch adds. Membership is all that test asserts, so it passed while the contract was incomplete. * desktop: state 0700 on the debian update staging directory install_verified_package verifies the signature as root, writes the package into a tempdir under /var/tmp, and hands that path to apt-get. The window between those two steps is only safe if nobody else can write in that directory, and the mode was left to the umask: tempfile 3.27.0 sets a directory mode only when Builder::permissions was called, so it creates 0777 masked by the umask, and pkexec does not reset the umask, it only rebuilds the environment. The calling session's umask therefore reaches the privileged helper. Measured on Ubuntu 24.04 by reporting the staged path from a stand-in apt-get: umask 022 gave dir 0755 file 0644, umask 002 gave 0775 and 0664, and umask 000 gave 0777 and 0666. At 0777 a second local user can unlink the verified package and drop in another signed release, or any file at all, and root then installs it, which is exactly what verifying as root was meant to prevent. Confirmed by doing it: a non-root user replaced the staged package with a different version. Stating 0700 on the directory and 0600 on the file makes it 0700/0600 at every umask above. apt still installs through it: the local .deb copy falls back to root rather than the _apt sandbox user, with no warning and no failure, and the whole install matrix still passes, including the beta to stable promotion, the DPkg::Pre-Invoke concurrent-change guard and the held frontend lock. * desktop: pin the polkit action in a test that actually runs The dpkg -L assertion added earlier lives in the clean machine lane's "desktop linux deb" job, which is gated on github.event.pull_request.head.repo.fork != true. Every staging replica of this repo is a fork of unslothai/unsloth, so that job skips there and the lane went green on this branch without ever building a .deb or reaching the assertion. It still guards a real pull request upstream, which is the case that matters, but a check that cannot run where the change is rehearsed is worth pairing with one that can. So pin the mapping statically too, next to the AppImage files map that is already pinned in this file for the same reason: the tauri.conf.json entry that places the action, auth_admin on all three implicit cases, and both annotations, cross-checked against the crate name tauri derives /usr/bin/unsloth-studio from and the INSTALL_ARGUMENT main.rs dispatches on, so the policy and the code cannot drift apart silently. Checked against removing the files map, weakening one auth_admin to yes, repointing exec.path and repointing exec.argv1: each fails it. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * desktop: require apt-get before offering an in-app debian update bundle_type() is baked into the binary by the bundler, not derived from the running system, so a .deb unpacked onto a non-apt distro by hand or by alien still reports Deb. is_supported_install() checked the binary, its ownership and pkexec, but not the package manager it actually drives, so on such a system the app moved from ManualLinuxPackage to InApp and offered an update that then died on the first dpkg-query with "No such file or directory (os error 2)". Before this branch those users were sent to the release page, which worked. Measured on fedora:44 with the .deb's binary force-installed and a stand-in pkexec present, so the missing package manager was the only variable: before is_supported_install=true desktop_update_mode=InApp install fails: Debian package installation failed (exit status: 1): No such file or directory (os error 2) after apt_present=false is_supported_install=false desktop_update_mode=ManualLinuxPackage and if driven anyway: "The installed Debian updater or system authentication is unavailable. Install the package from the release page." install.rs:1166 already gates first-run dependency elevation on the same path for the same reason, so this keeps the two elevation paths consistent about where they work. No effect where apt-get exists: ubuntu 22.04, ubuntu 24.04, debian 12 and debian trixie all still pass the full install matrix 17/17, and the routing enumeration over the whole input space is 224 cells, 223 identical, 1 newly InApp, 0 narrowed, 0 violations. * desktop: tighten the comments added for the debian updater --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 14 天前 | |
Studio: add audio.cpp as a native engine for speech, music and dictation (#12342) * Studio: add audio.cpp as a native engine for speech, music and dictation audio.cpp (0xShug0/audio.cpp) runs 20+ audio model families natively on ggml. This adds it as a fourth native runtime beside llama.cpp, whisper.cpp and sd.cpp and makes its models usable on every audio surface: the Audio page (speech, music, transcribe), chat dictation and audio-upload transcription, read-aloud, Settings > Voice, and the OpenAI /v1/audio/speech, /v1/audio/transcriptions and /v1/models routes. - Runtime: studio/install_audio_cpp_prebuilt.py installs a self-contained audiocpp_server bundle (audio.cpp vendors its own patched ggml) from the pinned Etherll/audio.cpp release, verified against in-repo sha256 pins, with an upstream fallback, CPU fallback, offline/no-op reuse, a staged start check, and the prebuilt_core install lock. setup.sh/setup.ps1 run it fail-open; uninstall, the desktop app, Docker and CI know the new runtime. - Models: one curated catalog (audio_cpp_models.py, mirrored by audio-cpp-catalog.ts) of TTS, music and ASR packages from audio-cpp/audio.cpp-gguf, addressed by virtual ids and pinned to a tested revision. - Speech and music load into the main audio slot through the native-audio worker (AudioCppBackend); speech-to-text is a fourth dictation engine, "audiocpp". Both run one audiocpp_server child with Studio's usual lifecycle: loopback port, scrubbed env, per-launch readiness id, idle unload, cancel, training pre-emption. - A model the installed runtime cannot run (missing runtime, no eSpeak-ng build, a Windows path over MAX_PATH) is refused with a clear reason before any download or eviction. * Studio audio.cpp: fix Windows cancel, proxied readiness and link-farm cleanup - Cancel returns at once on Windows: the request runs on its own thread, since shutting the socket from another thread never wakes a blocked recv there. - The readiness probe uses http.client, so an ambient HTTP_PROXY cannot route loopback traffic away from the server. - An install from the upstream fallback asks for the fork again instead of matching offline; the fork lookup had only failed on that run. - Link-farm prune and materialize never follow or write through a symlink or junction planted in the farm, and prune keeps the empty folders of a downloaded model that a concurrent materialize has just made. - Clearing the hub cache and deleting the umbrella repo from a non-active cache prune the farm beside that cache, so the hardlinked bytes are freed. - The download plan of a fully downloaded model answers from disk, so it works offline. - The installer sweeps staging dirs a killed run left, normalises the accelerator setup forwards, and runs the staged server with secrets scrubbed like every other child. - Register audio_cpp_server.py in the shutdown-latch completeness test. * Studio audio.cpp: prune the farm of the hub root a repo delete targeted The Hub row sends the repo folder as cache_path, not a hub root, so the prune looked for a farm that does not exist and the deleted blobs stayed on disk. Resolve the hub root with scoped_delete_root, as the delete itself does. * Studio audio.cpp: parse the fake server's multipart without cgi (removed in Python 3.13) * Studio audio.cpp: refuse pinned Linux bundles on glibc < 2.35 before downloading; keep a user-chosen tag on the chosen repo * Studio audio.cpp: auto mode installs the CPU build when the Linux CUDA bundle has no NCCL (no-torch Studio) * Studio audio.cpp: report an unwritable link-farm location as runtime unavailable * Studio audio.cpp: keep the child's eSpeak-ng data dir within its path buffer under a long Studio root * Studio audio.cpp: stop the STT server when a transcription is cancelled mid-request * Studio audio.cpp: refuse deleting the umbrella repo while one of its models is loading * Studio audio.cpp: refuse an overlong Windows model path in the runtime preflight, before download or eviction * Uninstall: keep an unmarked ~/.unsloth/audio.cpp, as for sd.cpp * Tests: count audio.cpp's keep arm in the setup status contract setup.sh and setup.ps1 now carry one "keeping the existing complete install" arm per runtime: llama.cpp, whisper.cpp and audio.cpp. * Tests: stub the NCCL probe in the --print-asset resolution test On a Linux runner without NCCL, auto mode now resolves the CPU build only, so the test followed the runner rather than the CUDA host it describes. * Studio: treat audio.cpp models as ordinary audio GGUFs audio.cpp models were a curated catalog with virtual ids, their own download path and "audio.cpp" labels. They now go through the same GGUF plumbing as llama.cpp models, and the runtime is chosen at load time. - Detection by GGUF header: general.architecture "audiocpp" plus the embedded audiocpp.model_spec.family classify a repo as speech, music or speech-to-text, so any compatible Hugging Face repo works, not only a fixed list. llama-server refuses these files, and the Audio page refuses a GGUF that is not an audio model. - A family table (task, server task, eSpeak need, defaults, options, packages) replaces the model catalog. Families outside Studio's audio features are refused with a reason. - Umbrella folders keep their audio-cpp/audio.cpp-gguf/<Folder> ids; dedicated repos use their own ids. Quant variants come from /gguf-variants and download through the normal GGUF download, so rows read "<Folder> · <quant>". MiniMax Music 3 and YuE2 package repos list their component mixes; deleting one mix keeps files another still uses. - Per-model advanced options come from the model's spec and are rendered on the Audio page, saved per model and sent as audio_options. - YuE2 follows the requested length; missing required music fields are a 400. Speech-to-text accepts the /gguf-variants keys and keeps the loaded variant when none is named. - The UI shows -GGUF names with no audio.cpp branding. * Studio: keep saved dictation keys working with audio GGUF ids Settings > Voice and dictation store the built-in models by their short keys and compare them to /stt/status, which now reported folder ids, so a downloaded model showed as missing and never as ready. Status lists the saved keys whose files are downloaded and reports a model under the name the client loaded it with. User-facing errors say "the audio runtime" instead of naming the engine. * Studio: report the audio runtime fields as absent for llama.cpp loads The load response gained audio_family and audio_options, and the llama.cpp runtime-field builder requires every response field, so every chat GGUF load failed with "GGUF backend is missing runtime response fields". llama server never serves an audio GGUF, so it reports both as None. The worker source check now reads the mirrored-keys tuple instead of one spelling of it. * Studio audio.cpp: read per-major CUDA bundles and use them only when torch or the system has that CUDA runtime * Studio audio.cpp: install the unslothai/audio.cpp prebuilts, pinned to v0.8.2-audio8-perf-hotfix-unsloth.1 * Tests: stub the CUDA runtime probe in the --print-asset resolution test * Studio audio.cpp: pin v0.9.0-unsloth.1, fix MiniMax Music 3 requests, keep transducer ASR off CUDA graphs, cap GPU host threads, offer Qwen3-TTS CustomVoice speakers * Studio Audio: readable capability line, voice names and clip labels for GGUF audio models * Studio Audio: trim GGUF audio catalog comments * Studio audio.cpp: refuse repo file names that climb out of the link farm, keep music GGUF rows out of Chat, key resolution cache by credential * Studio dictation: skip the gc pass when releasing an empty Whisper sidecar and the link-farm prune on warm audio.cpp requests * Studio audio.cpp: prune the link farm beside every remembered cache after a delete * Studio dictation: move an audio.cpp server training put on the CPU back to the GPU once training ends * Studio audio.cpp: key remote GGUF headers by credential, and treat missing eSpeak data as an incomplete install * Studio audio.cpp: count missing model files in refs/main, where downloads land * Tests: scan the fixture cache in the music-row chat test * Studio audio.cpp: an Auto load on a CPU-only runtime is a CPU placement * Studio audio.cpp: record a CPU-only runtime's Auto load as resident in CPU RAM * Studio audio.cpp: hand a CPU-only runtime's Auto load to the worker as a CPU load * Studio audio.cpp: report variant STT downloads by row, and relist downloads after one finishes * Studio audio.cpp: launch a custom build on a backend it was compiled with * Studio audio: YuE2 generates without lyrics, and a custom GGUF dictation repo skips the Whisper validator * Studio audio: remember the picked STT quant with its repo across restarts * Studio audio.cpp: trim comments * Studio audio.cpp: name the --version probe's encoding; teach main's contract tests the audio fields and variant downloads --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 1 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix Studio desktop reliability (#7255) * Fix Studio desktop reliability * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix desktop export completion and layout migration * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix maximized setup layout migration * Adapt desktop exports to data settings * fix(studio): harden desktop reliability edge cases * fix(studio): preserve rounded combobox focus fill --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | 2 个月前 | |
Rebrand Tauri desktop app as Unsloth (#7608) * Rebrand Tauri desktop app as Unsloth * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Preserve desktop package upgrade identity * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix legacy Windows desktop upgrades * Respect UI font scaling in Tauri branding * Use readable desktop release filenames * Set default UI font size to 15 * Compact sidebar chat rows * Set sidebar chat rows to 29px * Set sidebar chat rows to 30px --------- 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> | 1 个月前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Fix desktop icon clarity with supplied artwork and Tauri resource icons (#12352) * Fix desktop icon clarity with supplied artwork and Tauri resource icons * Declare Windows DPI awareness before loading runtime icons * Tighten Tauri icon test comments --------- Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com> | 2 天前 | |
Reduce antivirus false positives in the desktop installers (#8586) * Windows setup: install uv from a pinned release instead of running remote script text studio/setup.ps1 piped astral's install.ps1 straight into Invoke-Expression. That download-and-execute shape is the single construct AMSI providers and cloud ML scanners score hardest, and install.ps1 already replaced it with a pinned-SHA-256 archive download. Port the same implementation across. Progress goes to the pipeline rather than the console, so the quiet path swallows it exactly as it swallowed astral's installer output and the printed lines around the call site are unchanged. * Windows: stop pairing a hidden window with a bypassed execution policy The Studio shortcut launched launch-studio.ps1 with -WindowStyle Hidden and -ExecutionPolicy Bypass on the same command line. That pair is what Microsoft's own detections key on, and studio/src-tauri/src/install.rs already refuses it for the app's own launch of install.ps1. The installer writes launch-studio.ps1 itself, so the file carries no mark-of-the-web and RemoteSigned loads it. The hidden window is unchanged, so the shortcut behaves exactly as before. The generated launcher's own child launch moves to RemoteSigned for the same reason: it runs an inline -Command against an executable, where no script file is loaded and the two policies are equivalent. Also refresh a stale comment in studio/setup.ps1 that attributed the PSModulePath fix to astral's uv installer, which no longer runs in-process. * Installers: keep download-and-run command lines out of the shipped script text AMSI scans install.ps1 in full before a single line of it runs, and generic script classifiers read install.sh the same way inside the Linux bundle. Both headers rehearsed the piped web one-liner five times over, plus a scriptblock form and an execution-policy bypass, none of which anything in the scripts reads and all of which the README already documents. Point at the README instead and reword the in-body comments that quoted the one-liner as shorthand. Every printed line is untouched: the remediation text the installers show users still spells out the command in full. Same treatment for scripts/uninstall.ps1's header. * Windows: resolve process image paths with one Win32_Process query install.ps1's venv-holder probe opened a handle to every running PID through inline C# compiled at runtime. Opening a handle per process is a shape AV heuristics score hard, and it bought nothing: Win32_Process reports ExecutablePath for exactly the processes those handles could be opened against, and answers for all of them in a single query instead of once per PID. The remaining file-canonicalisation imports stay -- handle-based resolution of linked ancestors has no faithful Windows PowerShell 5.1 equivalent, and it runs on security-relevant paths. Falls back to the per-process .Path when the query is unavailable, so a degraded WMI repository degrades exactly as the old code did on a process it could not open. * Desktop: say who blocked the install when AMSI stops the script PowerShell hands the whole top-level script block to AMSI while compiling it, so a security product's verdict arrives as a parse error over the entire file before install.ps1 runs a statement: no [TAURI:ERROR] marker, no phase log, and a stderr tail the user cannot act on. unsloth#8523 shows what that looks like in the UI -- "Installation failed: + FullyQualifiedErrorId : ScriptContainedMaliciousContent". Recognise the two stable error ids on either stream and append what the user actually needs: nothing was installed, nothing was changed, it is a false positive, update definitions and retry, do not turn off endpoint protection. The raw id stays in the message, because the diagnostics report and any vendor submission both need it. Matches the id, never the message text, which is localized, and tolerates the cmdlet suffix the Invoke-Expression form carries. * Desktop: ship each bundle only the installer it can run resolve_install_script picks install.sh on unix and install.ps1 everywhere else, but the shared Tauri config bundled both into every target. The Linux AppImage therefore carried 280 KB of Windows PowerShell it can never execute -- and it is the largest script body a generic classifier walking the squashfs reads, which is where Microsoft's Trojan:Script/Wacatac.B!ml verdict on 0.1.701-beta landed. Move the resource map into the per-platform configs. The clean-machine job already fails when a Linux bundle ships no install.sh; it now also fails when one ships install.ps1, so the split cannot silently regress in either direction. The .deb scanned clean with the same payload, so this is surface reduction rather than a proven fix for that verdict. * POSIX installers: install uv from a pinned release before falling back install.sh downloaded astral's install.sh to a temp file, ran it and deleted the file; studio/setup.sh piped it straight into a shell. Both are, shape for shape, what a dropper does, and generic ML script classifiers score them accordingly -- the 0.1.701-beta Linux AppImage came back Trojan:Script/Wacatac.B!ml while the .deb carrying the same scripts came back clean. Fetch the pinned release archive and verify a hardcoded SHA-256 instead, matching what install.ps1 already does on Windows. Only the four mainstream targets are pinned: musl, armv7 and any host without a digest tool keep the path they have today, because guessing a target triple wrong would break the install outright and that costs far more than the heuristic score of the fallback. Destination, PATH handling and every printed line are unchanged, so a host that takes either path ends up in the same state it did before. * tests: pin the installer shapes antivirus heuristics score One file collecting what was removed, so it cannot drift back: no remote script run in-process, no encoded or base64 payload, no hidden window paired with a bypassed execution policy, no handle opened against another process, and no new runtime-compiled native import outside an allowlist that carries a reason for each entry that stays. The last test is the other half of the contract. Hardening must not change what a user sees, so the remediation lines the installers print -- which still spell out the web one-liner in full -- are asserted verbatim. Removing the one-liner from comments is the point; removing it from what the user is told to run would be a regression. Runs on the existing discovery-based pytest step, no workflow list to update. * release: emit a false-positive submission packet for whatever gets flagged The build job assembles a Microsoft submission packet, but only for the Windows -setup.exe. The detection that actually arrived on 0.1.701-beta was Trojan:Script/Wacatac.B!ml on the Linux AppImage, so nothing was produced for the one asset that needed it. The VirusTotal job already knows which assets were flagged and by which engines, so put the packet there: hash, size and both portals, for every flagged asset whatever platform it came from, with a note that clearance is per hash and per vendor. Engine names are not repeated -- they are third-party text and already appear escaped under Flagging engines. The gate stays advisory; this only makes acting on it take seconds. * Revert "Windows: resolve process image paths with one Win32_Process query" This reverts commit 7897865c9. tests/python/test_windows_installer_concurrency_guard.py bans Get-CimInstance and $process.Path from Get-RunningStudioVenvProcesses outright, and requires the native image-path lookup. That contract came out of #7764, which closed a set of races where the installer inferred "in use" from something other than a confirmed executable identity and blocked installs that should have proceeded. Win32_Process.ExecutablePath does answer the same question, but a wrongly blocked install costs far more than the heuristic weight of three native imports. Record the imports in the AV-shapes allowlist with that reasoning instead, and keep the ban on the process-memory APIs, which the installer has no use for. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten the comments added by this branch Opening comment-reduction pass over the PR diff: same intent, fewer lines. Cut hardest on the prose that restated the PR description rather than explaining the code next to it. Comments and docstrings only, verified with comment_tools.py check --strip-docstrings across every Python file in the diff. * Drop an unused helper from the uv pinned-release test * Fix three review findings on the installer hardening Stray-resource check aborted the step it was meant to assert. grep exits 1 when it selects nothing, and under this step's set -o pipefail plus the runner's bash -e that kills the assignment outright, so every correctly split .deb failed clean-machine CI before reaching the check. Both lookups take || true now: no match is the passing case for the stray one, and for install.sh it was swallowing the explicit annotation in favour of a bare exit 1. studio/setup.sh skipped astral's XDG_DATA_HOME/../bin destination tier, which install.sh, install.ps1 and studio/setup.ps1 all honour. A host that configured an XDG location got uv under ~/.local/bin instead, where no later shell looks for it. The session PATH prepend hid it at install time. The AMSI guidance claimed nothing was changed even when the block landed on the nested studio/setup.ps1, which install.ps1 launches through the same inherited pipes after the venv, PyTorch and the packages are already on disk. Split the wording on whether a [TAURI:STEP] marker has been seen: a pre-start block produces none, so the reassurance is only given where it is true. * Key the submission packet on the flagged count, not the engine list stats and results are separate fields of the same VirusTotal response, so an asset can carry a flagged count with no readable results map. The summary table reports that asset and the packet skipped it, which is exactly the one that needs a packet. Select on stats.flagged and keep the engine list for the Flagging engines section, which is correctly keyed on having engines to name. * Drop the bundle stray-resource assertion from clean-machine CI That job downloads a published release, never a bundle built from the branch, so asserting the new resource split there turns every run red until a release ships with it. The split is a property of the Tauri config, and tests/studio/test_tauri_installer_resource_contract.py already enforces it at the right layer. The || true on the install.sh lookup stays: it is what lets the explicit annotation print instead of the step dying on grep's exit 1 under pipefail. * Cover the uv host matrix and repeat application in the pinned-release test The pinned path picks an archive per host triple, and a wrong pick installs a binary that cannot execute, which is worse than not installing at all. Drive _uv_pinned_asset over 20 host combinations and require each one to return its own triple or decline to the fallback. Also run the installer three times over one HOME and require an identical tree, and require a stale uv at the destination to be replaced rather than joined by a second copy: the installer is re-run on every upgrade and every repair. * Pick the pinned uv archive off a positive libc check, not the absence of musl An independent audit pass found the Linux selector accepts any host whose ldd output does not say musl. That is not the same question astral's installer asks: it checks a minimum glibc and drops to its musl-static archive below it, so three hosts that worked before this branch now get a GNU binary that cannot exec, and the helper reports success so the fallback never runs. aarch64 with glibc below 2.28 (Ubuntu 18.04) x86_64 with glibc below 2.17 (RHEL 6) a musl image with no ldd at all, where the probe simply finds nothing Read the version instead, from ldd or getconf, and require it to clear astral's floor for the triple. Anything unreadable declines to the fallback. Also ask the userland for its bitness rather than trusting uname on a 64-bit kernel running a 32-bit userland, and follow astral in reading hw.optional.arm64 so a translated shell under Rosetta 2 still gets the native macOS build. Three more from the same pass: Report success only when the destination uv is executable. A copy onto a busy or read-only destination could leave a file that is not, and reporting success there skipped the fallback. Nothing is unwound on the failure path on purpose: the fallback installs over whatever is at the destination, and deleting there would take out a working uv the host already had. Clear the mark of the web on the launcher we author. WriteAllText replaces the unnamed data stream and leaves other NTFS streams alone, so a launch-studio.ps1 that somehow carried one would keep it across the rewrite, and RemoteSigned refuses a marked unsigned script. Store the security-block kind and resolve its wording in message(). stdout and stderr are read by independent threads, so a [TAURI:STEP] written before a block can be observed after it, and freezing the wording at observation time could tell a user nothing was changed on a run that had already installed PyTorch. Also require the error id to appear as the value of a FullyQualifiedErrorId field, so a scanner log that merely names it cannot attach antivirus guidance to whatever fails next. The host matrix in the shell test grows to 28 rows covering every case above, and removing the new gate fails 8 of them. install.rs gains two tests: 37 pass. * Replace a symlinked uv destination instead of writing through it Three from the review on the previous head. cp onto a destination that is a symlink follows the link, so installing over `~/.local/bin/uv -> /opt/homebrew/bin/uv` rewrote the Homebrew binary in place and left the link pointing at a file another package manager owns. Stage next to the destination and rename over it: rename replaces the link itself, and it is atomic, so a concurrent reader never sees a half-written uv either. The staging file is removed when the rename fails, so a failed run leaves no debris. Verify the Windows copy the same way the shell scripts now do. Copy-Item is non-terminating under the caller's ErrorActionPreference, so a locked or ACL-denied destination let execution reach `$haveUv = $true` and the function reported success over whatever was already there. Compare the destination against the archive we just verified, so a stale uv.exe cannot pass for the one we meant to install. install.ps1 carried the same shape and gets the same treatment. Point the header links at the heading that exists. The README has no "Install Unsloth Studio"; it is "Unsloth Studio (web UI)", whose anchor is #unsloth-studio-web-ui. Three test cases cover the symlink: the file behind the link is untouched, the link itself is replaced, and no staging file survives. Reverting the fix fails two of them. * Fail the build when the uv pin drifts from a version floor Before the pin, astral's endpoint always delivered the newest uv, so raising UV_MIN_VERSION was safe on its own. It is not any more: a floor above the pin means a host with no uv gets 0.12.1 installed and then judged too old by the same script that installed it, on the one path where the pin is what runs. Two checks. All four installers must name the same uv, or which version a machine ends up with depends on which script reached it first. And the pin must clear every floor in the tree (UV_MIN_VERSION, UV_OFFLINE_MIN_VERSION, $UvMinVersion). Raising a floor past the pin fails the first, bumping one installer's pin alone fails the second. * Write the shell profile entry the pinned uv path no longer gets for free The P1 here is a real regression and it took a second look to see why. install.sh decides whether to add ~/.local/bin to the user's shell profile with `case ":$PATH:"`, near the end of the run. By then this process has prepended that directory twice, once for the uv bootstrap and once for the venv, so the guard answers yes for a login shell that would answer no and the profile line is never written. That was survivable while astral's installer ran, because it wrote its own profile line and its env file. The pinned path writes neither, so on a fresh account whose login PATH lacks ~/.local/bin the install succeeds, the current shell works, and the next terminal cannot find `unsloth` or `uv`. Snapshot the inherited PATH before anything prepends to it and test the guard against that. Two more from the same review. Honour a configured uv mirror exclusively. UV_INSTALLER_GHE_BASE_URL and UV_INSTALLER_GITHUB_BASE_URL already win outright in both PowerShell installers and in astral's own; the shell path ignored them and tried the public hosts first. A restricted network sets one precisely because those hosts are unreachable, and download() has no timeout, so it would hang rather than reach the fallback. Do not let the twin of an earlier clear erase a later AMSI verdict. Clear-TauriInstallError writes one logical clear to BOTH streams (install.ps1:198) and independent threads read them, so a block observed between a clear and its own twin was discarded by the twin. Ignore a clear identical to the one just processed; a genuine later recovery carries different text and still clears. Two tests cover both directions, 39 install tests pass. * Stage the uv copy under a per-process name An audit pass reproduced a race I introduced with the symlink fix. Both POSIX helpers staged through a fixed destination-side name, so two installers targeting one directory shared it: A finishes copying the staging file B opens the same path with truncation A renames that inode into place as uv B keeps writing through its open descriptor, which is now the published uv The published uv was observable at zero bytes until B resumed, which makes the claim in the comment about a concurrent reader flatly wrong. install.ps1 is covered by its named mutex, but nothing serialises the POSIX helpers, and studio/setup.sh runs standalone on every studio update. mktemp in the destination directory instead. Each rename then publishes a file no other process can still be writing, which is what the atomicity argument needed all along. The loser cleans up its own staging file and declines, so the caller falls back rather than reporting a success it did not achieve. * Keep a default install as quiet as it was when a uv mirror misbehaves Two console regressions from the audit pass, both on paths the install still recovers from. download() runs curl -LsSf, and -S deliberately prints its own errors. The fallback ran under run_maybe_quiet, so a failed download printed nothing before; the pinned attempts run outside that wrapper, so an unreachable mirror now put two curl: (N) lines on the console of a default install that then succeeded. Redirect stderr on the speculative attempts only, leaving download() untouched for every other caller. [TAURI:WARN] is a marker level install.sh has never emitted, and the app forwards unknown markers to its progress UI verbatim (install.rs:639), so a digest mismatch would have surfaced as raw text in the desktop window. Make it a verbose only stderr line: the next mirror or the fallback still runs, so a default install has nothing to say here. Printed-string diff against the merge base is back to additions inside $(...) capture plus that one verbose-gated line, with nothing removed or changed. * Ask the installed uv whether it runs before skipping the fallback The libc gate reads a glibc version from ldd or getconf and treats that as proof a GNU binary will execute. It is not. A stripped NixOS-derived image without nix-ld reports a glibc version through getconf while its loader lives in the Nix store, so the pinned x86_64 uv asks for /lib64/ld-linux-x86-64.so.2 and gets nothing. Every static check passed, so the helper reported success, the astral fallback was skipped, and the first real uv call failed with No such file or directory. astral's installer fails its own glibc probe on that host and ships the fully static musl archive, which runs. The user went from a working uv to none. The archive is digest-verified astral uv by the time it is placed, so ask it: run --version and require it to succeed. One exec closes the whole class rather than this one host, covering a wrong triple, a loader that is not where the binary looks, and a destination we could not really write. A test drives an archive whose uv cannot execute and requires the helper to decline; removing the exec check fails it. * Pair every clear with its twin, not just the previous one install.ps1 clears after each recovered step, so a lagging reader can be several clears behind when a block lands. With clears A then B on one stream and A's twin arriving on the other after the verdict, asking only whether this is the message just seen answers no, and the delayed twin discarded the verdict the guidance exists to explain. Each logical clear emits exactly two markers, so count unpaired ones by message: the first sighting is the clear, the next pairs with it. A test drives the A, B, verdict, A', B' ordering; 40 install tests pass. * Validate the staged uv before it replaces a working one My own exec check was on the wrong side of the rename. The sequence that bites: a host has a uv good enough for UV_OFFLINE_MIN_VERSION but below UV_MIN_VERSION, so the block runs with _uv_present_before true; the pinned path renames over that working binary; the --version check then fails because the loader is missing or the destination is mounted noexec; the fallback download also fails. The installer neither restores the old uv nor reports that none is available, and every later command runs the broken one. Test the staging file instead, before the rename. It sits on the destination filesystem, so it answers the noexec question too, and a binary that cannot run here never gets to replace one that could. Two tests: a working incumbent uv survives an archive whose uv cannot execute, and the rejected staging file is cleaned up. Moving the check back after the rename fails the first. * Stop the AMSI guidance claiming more than it knows Two of these are honesty defects in text a blocked user reads. "nothing was changed on this machine" is false. Rust starts a diagnostics attempt and its phase log before PowerShell is ever spawned, and spawn_script can create ~/.unsloth first, so a pre-start block has already written to disk. The honest claim is that no installation step ran. "This is a false positive" is not something the classifier can know. It proves the output carries a PowerShell error id and nothing about the script's integrity, and install.ps1 can sit in a user-writable directory, so a locally modified copy can earn a genuine verdict. Telling someone to report a correct detection to their vendor is worse than telling them to reinstall from an official package first and only escalate if an unmodified copy is still blocked. Two smaller ones from the same pass. The matcher tested for the field name and the id independently, so a line naming both in prose qualified; it now requires the id to follow the colon and end at a comma or whitespace, which is what the comment always claimed. And the clear-pairing map is bounded: legitimate producers use a small fixed label set, and child output must not be able to grow it without limit. 42 install tests pass. * Honour astral's download override, and stop Unblock-File asking Three from the second audit round. Unblock-File declares SupportsShouldProcess at the default Medium impact, so a profile that sets $ConfirmPreference to Medium or Low gets a prompt from the line I added, even for a launcher that never carried the stream. -ErrorAction does not suppress a ShouldProcess prompt, and a noninteractive host turns it into an error that skips shortcut setup entirely. -Confirm:$false. UV_DOWNLOAD_URL and its older alias INSTALLER_DOWNLOAD_URL outrank the mirror variables in astral's installer, and the merge-base path inherited that because it ran astral's script. All four implementations now honour them first and exclusively. My earlier comment argued they point at a version the pin would reject, but that reasoning had it backwards: a host sets one because it cannot reach the public endpoints, so ignoring it meant public egress first and, with no timeout on the download, a hang instead of a fallback. The pin still applies, so a source serving a different build fails the digest and the caller falls back to astral's installer, which honours the same variable. chmod 0755 on the staging file rather than +x. cp gives it the umask default and +x then adds execute only where the umask allowed read, so a umask of 077 left uv unusable for every other account on a shared machine. astral ships them 0755. Four checks pin the override precedence across all four installers and the mode across both shell ones, with the behaviour verified against a stubbed downloader. * Validate uv before it replaces an incumbent on Windows, and bound the probe install.ps1 and studio/setup.ps1 copied the extracted uv.exe straight over the destination and only asked whether it ran afterwards. A host with a working older uv and a policy (AppLocker, WDAC, endpoint protection) that refuses the new one was left with neither. Run the extracted binary where it landed first, then keep a copy of the incumbent across the publish and restore it if the published copy will not run, since Windows has no atomic replace for a file that may be open. The probe itself is bounded: Start-Process with a 20s WaitForExit and redirected streams, and on POSIX no stdin plus a 20s ceiling where timeout exists. A binary this installer just downloaded must not be able to hang an unattended install by prompting or by never exiting. install.sh and studio/setup.sh also published the pinned uvx after rejecting the pinned uv, leaving a pairing that is never built or tested. A uv that fails to stage, copy or run now abandons the whole placement. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Verify each uv mirror, and persist PATH when the account has no rc file Both PowerShell installers checked the archive digest once, after the download loop had already broken out. A captive portal or a proxy answering 200 with its own body is a successful download by every measure Invoke-WebRequest has, so the first mirror consumed the only attempt and the second, healthy one was never tried. The digest now decides whether a mirror counts as served. install.sh picked a shell profile from .zshrc, .bashrc or .profile and did nothing when none existed. A fresh account has none: astral's installer used to create its own PATH setup there, the pinned path does not, so the next terminal resolved neither unsloth nor uv. Fall back to creating ~/.profile, which every POSIX login shell reads. The existing content guard keeps it written once. * Remove the install.sh a Windows upgrade would otherwise keep forever Windows bundles now carry only install.ps1, but NSIS writes the current resource manifest and deletes nothing, and the uninstaller deletes only what is in that manifest. An in-place upgrade from a release that bundled both installers left install.sh in $INSTDIR permanently, which also made the non-recursive RMDir "$INSTDIR" fail at uninstall. The pre-install and pre-uninstall hooks now delete it, so the population most likely to upgrade actually gets the split. Also silence the speculative mktemp -d in the pinned uv path: its failure falls back to astral's installer, so an unusable TMPDIR printed a line the user could not act on and that the merge base did not print. * Remove the pinned uv temporaries when an install is interrupted The pinned path unpacks a 40 MB archive into a work directory and stages the binary next to the destination, but only cleaned both up when the helper returned normally. A Ctrl-C in between left the archive behind and left a staging file inside a directory that is on PATH. Both paths are now published to the exit and signal traps as they are created and cleared when the helper releases them. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Persist the PATH the way each shell actually reads it, and fail a half-published pair Four follow-ups from review: A uvx that the archive carried but that could not be staged or renamed left uv published next to a stale or missing uvx and still reported success, skipping the fallback that would have installed both. Either half failing now fails the placement, in install.sh and studio/setup.sh. studio/setup.sh had none of the interrupt cleanup install.sh gained: a Ctrl-C left the unpacked archive behind and a staging file inside a directory on PATH. It now owns HUP, INT, TERM and EXIT for the duration of the pinned install and hands them back on the way out. fish sources none of the POSIX rc files, so the ~/.profile fallback was a no-op for a fish user. The persistence helper writes a conf.d drop-in with fish_add_path there, and honours ZDOTDIR for zsh. UV_INSTALL_DIR, UV_UNMANAGED_INSTALL, XDG_BIN_HOME and XDG_DATA_HOME can put uv somewhere other than ~/.local/bin, and astral's installer wrote a PATH line for whichever it picked. The pinned path now persists its own destination too, with UV_NO_MODIFY_PATH honoured as astral honours it. * Make the Windows uv publish a real transaction, and quote persisted paths The companion copies ran bare: under install.ps1's Stop preference a locked or ACL-denied destination threw past the rollback and left a mismatched set with the backups still on disk, and under setup.ps1's Continue preference it kept a stale companion and reported success. Both now copy under -ErrorAction Stop inside the transaction, so any failure unwinds like the others. A failed restore also used to delete the backup anyway, which is the one path in this block that could leave the host with less than it started with: the two things that make a restore fail, an open incumbent and a denied ACL, are the same two that made the replace risky. The backup is now kept and named. fish takes an unquoted path with a space as two directories, neither of which exists, so the drop-in single-quotes it; and the rc line is written inside double quotes, so a uv directory holding a dollar or a backtick is escaped. The second test caught a doubled backslash in the escaper itself. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten the comments added by this branch Comments only, no code touched: 161 comment lines become 106 across install.sh, install.ps1, studio/setup.sh, studio/setup.ps1 and the NSIS hooks. Each one keeps the reason it was written for, said once. Verified with the PowerShell AST parser, sh -n and bash -n, the 50-check uv pinned release suite and 114 installer tests, and by confirming the diff contains no non-comment line. * Tighten the install.rs comments too Comments only: 35 lines become 27, each keeping the reason it was written for. 42 install tests pass and the diff contains no non-comment line. * Abort on a companion that cannot be backed up, and pair clears by stream A uvx.exe that could not be copied aside, because it is locked or its ACL denies reads, was skipped and the new uv.exe published anyway, so the function reported success with a mismatched pair and the fallback never ran. Any backup failure now fails the placement and runs the rollback, in install.ps1 and studio/setup.ps1. The ERROR_CLEAR pairing keyed only on the message, so two real clears of one label on one stream were taken for a clear and its twin. That happens: _install_torch_default_index emits its recovery during the install and again during the ROCm repair. A verdict landing between them was then erased by the genuinely later clear arriving on the other stream. The map is keyed by stream as well, so only the opposite stream's copy can consume a pending marker. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Do not fail an install because the uv probe could not get an answer Three clean-machine CI legs that pass on main failed on this branch: arm64 and two Windows containers, all three with winget unavailable, which is the only condition under which the pinned fallback runs. Each downloaded the right asset, passed the digest, and then failed the probe. Start-Process -NoNewWindow with redirected streams does not behave in a container or on the arm64 image the way it does in a desktop session, and a boolean probe reported that as a broken binary and aborted the install. The probe is now tri-state. Only the binary answering non-zero is a failure. A launch that throws or a wait that times out is inconclusive, and since the digest already proved the bytes are astral's pinned release, an inconclusive probe publishes as the pre-pin code did. Every path prints why, with the captured stderr and the exit code, so the next occurrence is not opaque. Also from review: the POSIX path now stages both binaries and publishes them together with the incumbents saved aside, so a failed uvx rename restores the uv it replaced instead of leaving a new uv beside a stale uvx; the Windows rollback records the destination before the copy that can truncate it; and UV_UNMANAGED_INSTALL suppresses the profile write, as it does for astral. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Give setup.sh the same pair publish and PATH persistence as install.sh studio/setup.sh published uv and then uvx one after the other, so a failed uvx rename left a new uv beside the host's stale one, and the remote fallback can be unavailable. It now stages both, validates uv, and publishes the two renames back to back with the incumbents saved aside, restoring them if the second fails. setup.sh is also run directly for local and Colab setup, where astral's installer used to write the profile line for whichever destination it chose. Without one the PATH export died with that shell and every later run reinstalled uv. It now persists its own destination, with fish handled on its own terms and both of astral's opt-outs honoured. * Treat an empty uv exit code as no verdict, not as a failure The arm64 clean-machine leg still failed on the tri-state probe, and the diagnostic that came with it said why: "uv --version exited ." with no number. WaitForExit(ms) can return before the exit code is cached, so ExitCode was empty and an empty value is not 0, which read a working uv as broken. The parameterless WaitForExit settles it and returns at once because the process has already exited, and a code that is still missing is inconclusive rather than a failure, which is the same rule the launch and timeout paths already follow. Verified against pwsh that a real non-zero exit and a real launch failure still classify as failed and unknown respectively. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Eight review fixes across the uv publish and PATH persistence The fish escaper in studio/setup.sh reached sed as an invalid expression, so a fish user running setup directly would have had setup killed under set -e right after uv was published. It now matches the one in install.sh, and the test runs both escapers rather than reading them. An incumbent that cannot be hard-linked or copied cannot be restored either, so publishing over it would be a one-way move. Both shells now decline. Writing that test turned up that my own rollback deleted both incumbents when nothing had been published, since the no-predecessor branch cannot tell the two cases apart; the rollback is now reached only after a publish was attempted. A rollback with no predecessor removes the binary it published, rather than leaving half a pair the host never had. A signal between the two renames left the undo copy as the only reference to the incumbent, and the handler deleted it. It restores it now, in both shells. setup.sh prepended ~/.local/bin unconditionally after a successful pinned install, so a stale uv there could shadow a custom UV_INSTALL_DIR destination and the rest of setup would run the wrong one. That prepend is now only for astral's installer, which is what writes there. PATH entries are compared literally rather than as case patterns, so a destination holding *, ? or [ is not mistaken for an unrelated entry. On Windows, a .unsloth-old left behind by a failed restore is the only copy of a working uv, and the next run reused that exact name. It takes a distinct one. * Keep the pinned uv first on PATH, and only count an active profile entry install.sh prepends ~/.local/bin after the uv bootstrap, and astral's env file does too, so a custom UV_INSTALL_DIR destination was pushed behind a stale uv sitting in the home directory and every bare uv below picked the wrong one. The pinned destination goes back in front. setup.sh had the same shape and was fixed in defd2292a. The profile check treated any occurrence of the destination text as proof the PATH entry was already there, so a commented-out old export, or /opt/uv-old when the destination is /opt/uv, suppressed the write and left the next shell without uv. Comments are stripped and the directory has to appear as a whole entry. * Gate the NSIS tidy-up, and remove an orphan uv on signal The pre-install hook runs before the user can still cancel, and $INSTDIR can be a directory they picked in the GUI, so deleting install.sh there could take a file that was never ours. Both hooks now only act where our own executable already is. A signal between the two renames restored a predecessor but did nothing when there was none, leaving a 0.12.1 uv beside whatever uvx the machine had. It now removes what it published, which is what the ordinary rollback already does. * Write the uv PATH entry to every startup file astral's installer wired astral's uv installer wires ~/.profile, each of .bashrc, .bash_profile and .bash_login that exists, .zshrc or .zshenv under ZDOTDIR, and a fish drop-in under ~/.config. Replacing that installer with a pinned archive meant the PATH entry only reached the one file for whichever shell happened to be running, so a bash user whose .bash_profile does not source .bashrc, a /bin/sh login, or anyone who later switched shells would have no uv on PATH where they used to. Both POSIX installers now write the same set, once each, with the existing whole-entry check keeping a re-run idempotent. Files that do not exist are not created, apart from ~/.profile, which astral creates too. * Cut the uv publish back to what the common case needs The rollback machinery that grew over the review rounds covered cases a user is very unlikely to meet: an incumbent that cannot be hard-linked, a signal landing between two renames, a restore that itself fails, a second installer racing the first. It was 281 net lines, and every finding in the last two rounds was in it rather than in the hardening. What stays is what the common case needs. POSIX stages both binaries, runs the staged uv, and publishes the pair with two renames; a failure anywhere before them leaves the destination untouched, and the caller falls back to astral's installer exactly as before. Windows probes the extracted uv.exe before touching the destination, then copies the three under -ErrorAction Stop and re-checks the digest at the destination. The staging files are still removed on a signal, since they live in a directory that is on PATH. 64 shell checks and 114 installer tests cover the rest. * Match the exact fish entry, and let a UNC launcher load The fish drop-in is the only thing that puts uv on a fish user's PATH, since fish reads none of the POSIX files, and its check treated any occurrence of the directory as proof: /opt/uv-old suppressed /opt/uv. It now matches the exact fish_add_path line it would write. A launcher on a UNC share is a remote script to PowerShell, and RemoteSigned refuses an unsigned one, so a roaming profile got a shortcut that exits without starting Studio. That case, and only that case, uses Bypass, and drops -WindowStyle Hidden with it so the pair the detections key on never appears. * Wire every startup file on a DEFAULT install too, and give setup.ps1 a fallback The all-profile PATH write was gated on the uv destination differing from ~/.local/bin, which is exactly where a normal install puts it, so every ordinary machine still got the single-file write the shim path has always done. Three independent audits found this. The gate is gone, and the idempotency check now also matches the $HOME-relative spelling the shim block writes, so the default case does not end up with two lines for one directory. studio/setup.ps1 replaced astral's installer with the pinned archive and had nothing to fall back to. A failed pinned install therefore left UseUv false and silently ran torch, bitsandbytes, Triton and the rest through pip: a different resolver, not just a different download. winget is the fallback, as install.ps1 already does, rather than the remote script this branch exists to remove. * Read the pinned install's real result, and three narrower publish guards The winget fallback I added last round read Invoke-SetupCommand's return value, which is [int]$LASTEXITCODE rather than the function's $true, so it fired on every run: a redundant managed install, and a second copy on a machine that asked for UV_UNMANAGED_INSTALL. The function records its own success on the script scope and the fallback reads that. A directory named uv at the destination looked like a published binary: mv moves into it and reports success, and a searchable directory passes -x, so the install reported success and the first later uv call failed instead. Both shells refuse a directory target, as the installer already does for its own shim. The PATH idempotency pattern escaped only part of the ERE metacharacter set, so a destination holding + ( or | did not match itself and every reinstall appended another block to every profile. * Restrict the profile duplicate check to PATH lines, and cover mapped drives for PR #8586 * Match only PATH-setting lines in the profile duplicate check for PR #8586 * Tighten the installer comments added by PR #8586 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com> | 1 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 19 天前 | ||
| 1 个月前 | ||
| 2 天前 | ||
| 14 天前 | ||
| 1 天前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 2 天前 | ||
| 1 个月前 |