Wire Updates NVIDIA card to bos-nvidia-setup

The Install driver button runs the real setup instead of only
pacman_install. Prefers pkexec bos-nvidia-setup; falls back to the
allowlisted packages plus the same hyprland.lua drop-in. Tells the
user to reboot. Still click-only — first boot does not install.
This commit is contained in:
Breadway 2026-08-16 00:54:40 +08:00
parent 0798fad697
commit 4e9cb217c7
3 changed files with 167 additions and 9 deletions

View file

@ -23,6 +23,7 @@
gpu: string;
reason: string;
packages: string[];
installed: boolean;
}
interface UpdatesStatus {
pacman: PendingUpdate[];
@ -64,16 +65,18 @@
<div class="offer-text">
<strong>{status.nvidia.gpu}</strong>
<p>{status.nvidia.reason}</p>
<Hint
text="Installs nvidia + nvidia-utils only. Afterward, Hyprland usually needs env = LIBVA_DRIVER_NAME,nvidia / __GLX_VENDOR_LIBRARY_NAME,nvidia / NVD_BACKEND,direct — not written for you. Reboot after install."
/>
{#if status.nvidia.installed}
<Hint text="Driver and Hyprland env drop-in are in place. Reboot to start a working session." />
{:else}
<Hint text="Installs nvidia + nvidia-utils (not cuda) and writes ~/.config/hypr/nvidia.lua. hyprland.lua loads that file only if it exists. Reboot after." />
{/if}
</div>
<button
class="primary"
disabled={busy}
onclick={() => run("pacman_install", { packages: status?.nvidia?.packages ?? ["nvidia", "nvidia-utils"] })}
onclick={() => run("nvidia_setup")}
>
Install driver
{status.nvidia.installed ? "Re-run setup" : "Install driver"}
</button>
</div>
</Group>

View file

@ -3,23 +3,64 @@
//! not install anything until the user clicks.
use serde::Serialize;
use std::path::{Path, PathBuf};
use tauri::AppHandle;
use super::streaming;
use super::util::{self, command_exists, pacman_installed};
/// Same drop-in bos-nvidia-setup writes. hyprland.lua dofiles it only
/// when the file exists (Mesa machines have no file).
/// Hyprland 0.56 (Aquamarine): wiki requires LIBVA + GLX vendor.
/// NVD_BACKEND is the current VA-API hint. No WLR_* / GBM_BACKEND.
const NVIDIA_LUA: &str = "\
-- Written by bos-nvidia-setup. hyprland.lua dofiles this only when it exists.
-- Hyprland 0.56 (Aquamarine) no WLR_* variables.
-- https://wiki.hypr.land/Nvidia/
hl.env(\"LIBVA_DRIVER_NAME\", \"nvidia\")
hl.env(\"__GLX_VENDOR_LIBRARY_NAME\", \"nvidia\")
hl.env(\"NVD_BACKEND\", \"direct\")
";
const HYPR_INCLUDE: &str = "\
-- bos-nvidia-setup: optional proprietary env; no-op when the file is absent
do
local nvidia = (os.getenv(\"HOME\") or \"\") .. \"/.config/hypr/nvidia.lua\"
local f = io.open(nvidia, \"r\")
if f then
f:close()
pcall(dofile, nvidia)
end
end
";
const NVIDIA_PACKAGES: &[&str] = &["nvidia", "nvidia-utils"];
#[derive(Serialize, Clone)]
pub struct NvidiaOffer {
gpu: String,
reason: String,
packages: Vec<String>,
installed: bool,
}
fn offer_paths() -> Vec<std::path::PathBuf> {
fn offer_paths() -> Vec<PathBuf> {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
let state = std::path::Path::new(&home).join(".local/state/bos");
let state = Path::new(&home).join(".local/state/bos");
vec![
state.join("nvidia-offer.json"),
state.join("nvidia-probe.json"),
]
}
fn nvidia_dropin_path() -> PathBuf {
util::hypr_dir().join("nvidia.lua")
}
fn nvidia_ready() -> bool {
nvidia_dropin_path().is_file() && NVIDIA_PACKAGES.iter().all(|p| pacman_installed(p))
}
pub fn read_nvidia_offer() -> Option<NvidiaOffer> {
for path in offer_paths() {
if !path.is_file() {
@ -56,11 +97,12 @@ pub fn read_nvidia_offer() -> Option<NvidiaOffer> {
.collect::<Vec<_>>()
})
.filter(|p| !p.is_empty())
.unwrap_or_else(|| vec!["nvidia".into(), "nvidia-utils".into()]);
.unwrap_or_else(|| default_packages());
return Some(NvidiaOffer {
gpu,
reason,
packages,
installed: nvidia_ready(),
});
}
return Some(generic_offer());
@ -68,11 +110,16 @@ pub fn read_nvidia_offer() -> Option<NvidiaOffer> {
None
}
fn default_packages() -> Vec<String> {
NVIDIA_PACKAGES.iter().map(|s| (*s).to_string()).collect()
}
fn generic_offer() -> NvidiaOffer {
NvidiaOffer {
gpu: "NVIDIA GPU".into(),
reason: "BOS found an NVIDIA device. Install the proprietary driver only if you want it — nouveau stays otherwise.".into(),
packages: vec!["nvidia".into(), "nvidia-utils".into()],
packages: default_packages(),
installed: nvidia_ready(),
}
}
@ -81,8 +128,87 @@ pub fn get_nvidia_offer() -> Option<NvidiaOffer> {
read_nvidia_offer()
}
/// Install nvidia + nvidia-utils and write the Hyprland env drop-in.
/// Prefers `/usr/local/bin/bos-nvidia-setup` (ISO script) so package
/// install + env stay one path. Falls back to allowlisted pacman plus
/// the same drop-in when the script is not on this install yet.
#[tauri::command]
pub async fn nvidia_setup(app: AppHandle, session_id: String) -> bool {
let home = std::env::var("HOME").unwrap_or_default();
if command_exists("bos-nvidia-setup") {
let ok = streaming::run_hardcoded(
app.clone(),
session_id.clone(),
"pkexec",
&["bos-nvidia-setup", "--home", &home],
)
.await;
if ok {
streaming::emit_line(&app, &session_id, "reboot required");
}
return ok;
}
streaming::emit_line(
&app,
&session_id,
"bos-nvidia-setup not on PATH; installing via pacman and writing the env drop-in",
);
let packages = default_packages();
if !streaming::pacman_install(app.clone(), session_id.clone(), packages).await {
return false;
}
match write_nvidia_dropin() {
Ok(()) => {
streaming::emit_line(&app, &session_id, "wrote ~/.config/hypr/nvidia.lua");
streaming::emit_line(&app, &session_id, "reboot required");
true
}
Err(e) => {
streaming::emit_line(&app, &session_id, &format!("Error: {e}"));
false
}
}
}
fn write_nvidia_dropin() -> Result<(), String> {
let dir = util::hypr_dir();
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join("nvidia.lua");
super::config::atomic_write(&path, NVIDIA_LUA).map_err(|e| e.to_string())?;
ensure_hyprland_include()?;
Ok(())
}
fn include_already_present(text: &str) -> bool {
text.contains("nvidia.lua")
}
fn ensure_hyprland_include() -> Result<(), String> {
let path = util::hypr_dir().join("hyprland.lua");
let existing = std::fs::read_to_string(&path).unwrap_or_default();
if include_already_present(&existing) {
return Ok(());
}
if existing.is_empty() {
return Ok(());
}
let mut text = existing;
if !text.ends_with('\n') {
text.push('\n');
}
text.push('\n');
text.push_str(HYPR_INCLUDE);
if !text.ends_with('\n') {
text.push('\n');
}
super::config::atomic_write(&path, &text).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_file_is_none() {
// This machine's real probe path is not something the unit test
@ -103,4 +229,32 @@ mod tests {
assert!(hide(r#"{"dismissed":true}"#));
assert!(!hide(r#"{"gpu":"RTX 4060"}"#));
}
#[test]
fn dropin_has_current_wiki_env_and_no_obsolete_vars() {
assert!(NVIDIA_LUA.contains("LIBVA_DRIVER_NAME"));
assert!(NVIDIA_LUA.contains("__GLX_VENDOR_LIBRARY_NAME"));
assert!(NVIDIA_LUA.contains("NVD_BACKEND"));
assert!(!NVIDIA_LUA.contains("hl.env(\"WLR_"));
assert!(!NVIDIA_LUA.contains("hl.env(\"GBM_BACKEND"));
assert!(!NVIDIA_LUA.contains("cuda"));
}
#[test]
fn include_snippet_is_conditional() {
assert!(HYPR_INCLUDE.contains("nvidia.lua"));
assert!(HYPR_INCLUDE.contains("io.open"));
assert!(HYPR_INCLUDE.contains("pcall(dofile"));
}
#[test]
fn include_detects_existing_snippet() {
assert!(include_already_present("pcall(dofile, nvidia.lua)"));
assert!(!include_already_present("hl.env(\"XCURSOR_SIZE\", \"24\")"));
}
#[test]
fn packages_are_nvidia_and_utils_only() {
assert_eq!(NVIDIA_PACKAGES, &["nvidia", "nvidia-utils"]);
}
}

View file

@ -115,6 +115,7 @@ pub fn run() {
commands::breadhelp::open_breadhelp,
commands::updates::get_updates_status,
commands::nvidia::get_nvidia_offer,
commands::nvidia::nvidia_setup,
commands::printing::get_printers,
commands::printing::set_default_printer,
commands::printing::add_ipp_printer,