Initial work on Flatpak support

This commit is contained in:
Charlie Le 2024-11-19 14:20:22 -05:00
commit 2cd30019d1
10 changed files with 249 additions and 71 deletions

96
dist/flatpak/org.gabmus.envision.json vendored Normal file
View file

@ -0,0 +1,96 @@
{
"id": "org.gabmus.envision",
"branch": "47",
"runtime": "org.gnome.Sdk",
"runtime-version": "47",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm19"
],
"command": "envision",
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm19/bin",
"env": {
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER": "clang",
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold",
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "clang",
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold"
},
"build-args": [
"--share=network"
]
},
"cleanup": [
"/share/doc",
"/share/man"
],
"finish-args": [
"--share=ipc",
"--share=network",
"--socket=wayland",
"--socket=fallback-x11",
"--socket=pulseaudio",
"--device=all",
"--filesystem=xdg-run/pipewire-0",
"--filesystem=xdg-run/monado_comp_ipc",
"--filesystem=~/.steam",
"--filesystem=~/.var/app/com.valvesoftware.Steam",
"--talk-name=org.freedesktop.Flatpak"
],
"modules": [
{
"name": "OpenXR-SDK",
"buildsystem": "cmake",
"sources": [
{
"type": "git",
"url": "https://github.com/KhronosGroup/OpenXR-SDK-Source.git",
"tag": "release-1.1.42"
}
]
},
{
"name": "vte",
"buildsystem": "meson",
"config-opts": [
"-Dgtk4=true",
"-Dgtk3=false"
],
"sources": [
{
"type": "archive",
"url": "https://gitlab.gnome.org/GNOME/vte/-/archive/0.78.0/vte-0.78.0.tar.gz",
"sha256": "82e19d11780fed4b66400f000829ce5ca113efbbfb7975815f26ed93e4c05f2d"
}
]
},
{
"name": "eigen",
"buildsystem": "cmake",
"builddir": true,
"build-options": {
"config-opts": [
"-DEIGEN_BUILD_CMAKE_PACKAGE=NO"
]
},
"sources": [
{
"type": "git",
"url": "https://gitlab.com/libeigen/eigen.git",
"tag": "3.4.0"
}
]
},
{
"name": "Envision",
"buildsystem": "meson",
"sources": [
{
"type": "dir",
"path": "../../"
}
]
}
]
}

View file

@ -125,6 +125,8 @@ fn include_paths() -> Vec<String> {
vec![ vec![
"/usr/include".into(), "/usr/include".into(),
"/usr/local/include".into(), "/usr/local/include".into(),
// flatpak applications use /app
"/app/include".into(),
// fedora puts avcodec here // fedora puts avcodec here
"/usr/include/ffmpeg".into(), "/usr/include/ffmpeg".into(),
"/usr/include/x86_64-linux-gnu".into(), "/usr/include/x86_64-linux-gnu".into(),

View file

@ -1,8 +1,5 @@
use crate::{ use crate::{
paths::{get_backup_dir, SYSTEM_PREFIX}, is_flatpak::IS_FLATPAK, paths::{get_backup_dir, get_home_dir, SYSTEM_PREFIX}, profile::Profile, util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly}, xdg::XDG
profile::Profile,
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
xdg::XDG,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -138,17 +135,28 @@ fn relativize_active_runtime_lib_path(ar: &ActiveRuntime, path: &Path) -> Active
} }
pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_current_active_runtime_to_profile(profile: &Profile) -> anyhow::Result<()> {
let dest = get_active_runtime_json_path(); let mut dests = vec![
set_file_readonly(&dest, false)?; get_active_runtime_json_path(),
get_home_dir().join(".var/app/com.valvesoftware.Steam/config/openxr/1/active_runtime.json"),
];
if *IS_FLATPAK {
dests.push(get_home_dir().join(".config/openxr/1/active_runtime.json"));
}
backup_steam_active_runtime(); backup_steam_active_runtime();
let pfx = profile.clone().prefix; let pfx: PathBuf = profile.clone().prefix;
for dest in dests {
if !dest.is_file() {
continue;
}
let mut ar = build_profile_active_runtime(profile)?; let mut ar = build_profile_active_runtime(profile)?;
// hack: relativize libopenxr_monado.so path for system installs // hack: relativize libopenxr_monado.so path for system installs
if pfx == PathBuf::from(SYSTEM_PREFIX) { if pfx == PathBuf::from(SYSTEM_PREFIX) {
ar = relativize_active_runtime_lib_path(&ar, &dest); ar = relativize_active_runtime_lib_path(&ar, &dest);
} }
dump_current_active_runtime(&ar)?; set_file_readonly(&dest, false)?;
dump_active_runtime_to_path(&ar, &dest)?;
set_file_readonly(&dest, true)?; set_file_readonly(&dest, true)?;
}
Ok(()) Ok(())
} }

View file

@ -1,10 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::{ use crate::{
paths::get_backup_dir, is_flatpak::IS_FLATPAK, paths::{get_home_dir, get_backup_dir}, profile::Profile, util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly}, xdg::XDG
profile::Profile,
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
xdg::XDG,
}; };
use serde::{ser::Error, Deserialize, Serialize}; use serde::{ser::Error, Deserialize, Serialize};
@ -23,9 +20,19 @@ pub fn get_openvr_conf_dir() -> PathBuf {
} }
fn get_openvrpaths_vrpath_path() -> PathBuf { fn get_openvrpaths_vrpath_path() -> PathBuf {
if *IS_FLATPAK {
return get_home_dir().join(".config/openvr/openvrpaths.vrpath")
}
get_openvr_conf_dir().join("openvrpaths.vrpath") get_openvr_conf_dir().join("openvrpaths.vrpath")
} }
fn get_openvrpaths_vrpaths() -> Vec<PathBuf> {
vec![
get_openvrpaths_vrpath_path(),
get_home_dir().join(".var/app/com.valvesoftware.Steam/config/openvr/openvrpaths.vrpath"),
]
}
pub fn is_steam(ovr_paths: &OpenVrPaths) -> bool { pub fn is_steam(ovr_paths: &OpenVrPaths) -> bool {
ovr_paths.runtime.iter().any(|rt| { ovr_paths.runtime.iter().any(|rt| {
rt.to_string_lossy() rt.to_string_lossy()
@ -104,11 +111,15 @@ pub fn build_profile_openvrpaths(profile: &Profile) -> OpenVrPaths {
} }
pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> { pub fn set_current_openvrpaths_to_profile(profile: &Profile) -> anyhow::Result<()> {
let dest = get_openvrpaths_vrpath_path();
set_file_readonly(&dest, false)?;
backup_steam_openvrpaths(); backup_steam_openvrpaths();
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?; for dest in get_openvrpaths_vrpaths() {
if !dest.is_file() {
continue;
}
set_file_readonly(&dest, false)?;
dump_openvrpaths_to_path(&build_profile_openvrpaths(profile), &dest)?;
set_file_readonly(&dest, true)?; set_file_readonly(&dest, true)?;
}
Ok(()) Ok(())
} }

6
src/is_flatpak.rs Normal file
View file

@ -0,0 +1,6 @@
use lazy_static::lazy_static;
use std::path::Path;
lazy_static! {
pub static ref IS_FLATPAK: bool = Path::new("/.flatpak-info").is_file();
}

View file

@ -30,6 +30,7 @@ pub mod env_var_descriptions;
pub mod file_builders; pub mod file_builders;
pub mod gpu_profile; pub mod gpu_profile;
pub mod is_appimage; pub mod is_appimage;
pub mod is_flatpak;
pub mod linux_distro; pub mod linux_distro;
pub mod log_parser; pub mod log_parser;
pub mod openxr_prober; pub mod openxr_prober;

View file

@ -20,34 +20,21 @@ use crate::{
build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs, build_mercury::get_build_mercury_jobs, build_monado::get_build_monado_jobs,
build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs, build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
build_wivrn::get_build_wivrn_jobs, build_wivrn::get_build_wivrn_jobs,
}, }, config::Config, constants::APP_NAME, depcheck::{
config::Config,
constants::APP_NAME,
depcheck::{
basalt_deps::get_missing_basalt_deps, common::dep_pkexec, basalt_deps::get_missing_basalt_deps, common::dep_pkexec,
libsurvive_deps::get_missing_libsurvive_deps, mercury_deps::get_missing_mercury_deps, libsurvive_deps::get_missing_libsurvive_deps, mercury_deps::get_missing_mercury_deps,
monado_deps::get_missing_monado_deps, openhmd_deps::get_missing_openhmd_deps, monado_deps::get_missing_monado_deps, openhmd_deps::get_missing_openhmd_deps,
wivrn_deps::get_missing_wivrn_deps, wivrn_deps::get_missing_wivrn_deps,
}, }, file_builders::{
file_builders::{
active_runtime_json::{ active_runtime_json::{
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam, set_current_active_runtime_to_profile, set_current_active_runtime_to_steam,
}, },
openvrpaths_vrpath::{ openvrpaths_vrpath::{
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam, set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
}, },
}, }, is_flatpak::IS_FLATPAK, linux_distro::LinuxDistro, openxr_prober::is_openxr_ready, paths::get_data_dir, profile::{Profile, XRServiceType}, stateless_action, steam_linux_runtime_injector::{
linux_distro::LinuxDistro,
openxr_prober::is_openxr_ready,
paths::get_data_dir,
profile::{Profile, XRServiceType},
stateless_action,
steam_linux_runtime_injector::{
restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile, restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile,
}, }, util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd}, vulkaninfo::VulkanInfo, xr_devices::XRDevice
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
vulkaninfo::VulkanInfo,
xr_devices::XRDevice,
}; };
use adw::{prelude::*, ResponseAppearance}; use adw::{prelude::*, ResponseAppearance};
use gtk::glib::{self, clone}; use gtk::glib::{self, clone};
@ -56,7 +43,7 @@ use relm4::{
new_action_group, new_stateful_action, new_stateless_action, new_action_group, new_stateful_action, new_stateless_action,
prelude::*, prelude::*,
}; };
use std::{collections::VecDeque, fs::remove_file, time::Duration}; use std::{collections::{HashMap, VecDeque}, fs::remove_file, process::Command, time::Duration};
pub struct App { pub struct App {
application: adw::Application, application: adw::Application,
@ -175,9 +162,20 @@ impl App {
}; };
self.debug_view.sender().emit(DebugViewMsg::ClearLog); self.debug_view.sender().emit(DebugViewMsg::ClearLog);
self.xr_devices = vec![]; self.xr_devices = vec![];
if *IS_FLATPAK {
Command::new("flatpak-spawn")
.args(vec![
"--host",
"rm",
prof.xrservice_type.ipc_file_path().to_str().unwrap()
])
.output()
.expect("Failed to remove xrservice IPC file");
} else {
remove_file(prof.xrservice_type.ipc_file_path()) remove_file(prof.xrservice_type.ipc_file_path())
.is_err() .is_err()
.then(|| println!("Failed to remove xrservice IPC file")); .then(|| println!("Failed to remove xrservice IPC file"));
}
let worker = JobWorker::xrservice_worker_wrap_from_profile( let worker = JobWorker::xrservice_worker_wrap_from_profile(
&prof, &prof,
sender.input_sender(), sender.input_sender(),
@ -218,11 +216,28 @@ impl App {
let prof = self.get_selected_profile(); let prof = self.get_selected_profile();
if let Some(autostart_cmd) = &prof.autostart_command { if let Some(autostart_cmd) = &prof.autostart_command {
let mut jobs = VecDeque::new(); let mut jobs = VecDeque::new();
if *IS_FLATPAK {
let mut args = vec![String::from("--host")];
for (key, value) in &prof.environment {
args.push(format!("--env={}={}", key, value));
}
args.push(String::from("sh"));
args.push(String::from("-c"));
args.push(autostart_cmd.clone());
jobs.push_back(WorkerJob::new_cmd(
Some(HashMap::new()),
"flatpak-spawn".into(),
Some(args),
));
} else {
jobs.push_back(WorkerJob::new_cmd( jobs.push_back(WorkerJob::new_cmd(
Some(prof.environment.clone()), Some(prof.environment.clone()),
"sh".into(), "sh".into(),
Some(vec!["-c".into(), autostart_cmd.clone()]), Some(vec!["-c".into(), autostart_cmd.clone()]),
)); ));
}
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg { let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows), JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code), JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code),
@ -533,7 +548,7 @@ impl AsyncComponent for App {
.sender() .sender()
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Done)); .emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Done));
let profile = self.get_selected_profile(); let profile = self.get_selected_profile();
if dep_pkexec().check() { if dep_pkexec().check() || *IS_FLATPAK {
self.setcap_confirm_dialog.present(Some(&self.app_win)); self.setcap_confirm_dialog.present(Some(&self.app_win));
} else { } else {
alert_w_widget( alert_w_widget(
@ -606,11 +621,11 @@ impl AsyncComponent for App {
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone())); .emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
} }
Msg::RunSetCap => { Msg::RunSetCap => {
if !dep_pkexec().check() { if dep_pkexec().check() || *IS_FLATPAK {
println!("pkexec not found, skipping setcap");
} else {
let profile = self.get_selected_profile(); let profile = self.get_selected_profile();
setcap_cap_sys_nice_eip(&profile).await; setcap_cap_sys_nice_eip(&profile).await;
} else {
println!("pkexec not found, skipping setcap");
} }
} }
Msg::ProfileSelected(prof) => { Msg::ProfileSelected(prof) => {
@ -698,7 +713,7 @@ impl AsyncComponent for App {
let worker = JobWorker::new_with_timer( let worker = JobWorker::new_with_timer(
Duration::from_millis(500), Duration::from_millis(500),
WorkerJob::new_func(Box::new(move || { WorkerJob::new_func(Box::new(move || {
let ready = is_openxr_ready(); let ready = !*IS_FLATPAK && is_openxr_ready();
FuncWorkerOut { FuncWorkerOut {
success: ready, success: ready,
..Default::default() ..Default::default()

View file

@ -3,19 +3,12 @@ use super::{
state::JobWorkerState, state::JobWorkerState,
}; };
use crate::{ use crate::{
profile::{LighthouseDriver, Profile}, is_flatpak::IS_FLATPAK, profile::{LighthouseDriver, Profile}, ui::SENDER_IO_ERR_MSG
ui::SENDER_IO_ERR_MSG,
}; };
use nix::unistd::Pid; use nix::unistd::Pid;
use relm4::{prelude::*, Worker}; use relm4::{prelude::*, Worker};
use std::{ use std::{
collections::VecDeque, collections::{HashMap, VecDeque}, io::{BufRead, BufReader}, mem, os::unix::process::ExitStatusExt, process::{Command, Stdio}, sync::{Arc, Mutex}, thread
io::{BufRead, BufReader},
mem,
os::unix::process::ExitStatusExt,
process::{Command, Stdio},
sync::{Arc, Mutex},
thread,
}; };
macro_rules! logger_thread { macro_rules! logger_thread {
@ -217,11 +210,21 @@ impl InternalJobWorker {
vec![], vec![],
), ),
}; };
let data = CmdWorkerData { let mut data = CmdWorkerData {
environment: env, environment: env.clone(),
command, command: command.clone(),
args, args: args.clone(),
}; };
if *IS_FLATPAK {
data.environment = HashMap::new();
data.command = String::from("flatpak-spawn");
data.args = vec![String::from("--host")];
for (key, value) in env {
data.args.push(format!("--env={}={}", key, value));
}
data.args.push(command);
data.args.extend(args);
}
let mut jobs = VecDeque::new(); let mut jobs = VecDeque::new();
jobs.push_back(WorkerJob::Cmd(data)); jobs.push_back(WorkerJob::Cmd(data));
Self::builder().detach_worker(JobWorkerInit { jobs, state }) Self::builder().detach_worker(JobWorkerInit { jobs, state })

View file

@ -17,6 +17,7 @@ use crate::{
config::Config, config::Config,
depcheck::common::dep_pkexec, depcheck::common::dep_pkexec,
gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile}, gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile},
is_flatpak::IS_FLATPAK,
paths::{get_data_dir, get_home_dir}, paths::{get_data_dir, get_home_dir},
profile::{LighthouseDriver, Profile, XRServiceType}, profile::{LighthouseDriver, Profile, XRServiceType},
stateless_action, stateless_action,
@ -270,7 +271,7 @@ impl SimpleComponent for MainView {
add_css_class: "card", add_css_class: "card",
add_css_class: "padded", add_css_class: "padded",
#[track = "model.changed(Self::selected_profile())"] #[track = "model.changed(Self::selected_profile())"]
set_visible: match mount_has_nosuid(&model.selected_profile.prefix) { set_visible: !*IS_FLATPAK && match mount_has_nosuid(&model.selected_profile.prefix) {
Ok(b) => b, Ok(b) => b,
Err(_) => { Err(_) => {
eprintln!( eprintln!(
@ -302,7 +303,34 @@ impl SimpleComponent for MainView {
add_css_class: "card", add_css_class: "card",
add_css_class: "padded", add_css_class: "padded",
#[track = "model.changed(Self::selected_profile())"] #[track = "model.changed(Self::selected_profile())"]
set_visible: !dep_pkexec().check(), set_visible: *IS_FLATPAK,
warning_heading(),
gtk::Label {
set_label: concat!(
"Envision is currently running as a Flatpak.\n",
"If Steam is running as a Flatpak, it will need to be granted certain ",
"permissions to run VR applications. Run the following command on your host ",
"terminal to grant the Steam Flatpak access to Envision's Monado socket:\n\n",
"<tt>flatpak --user override --filesystem=xdg-run/monado_comp_ipc com.valvesoftware.Steam</tt>\n\n",
"Run the following to also grant the Steam Flatpak access to Envision's data:\n\n",
"<tt>flatpak --user override --filesystem=~/.var/app/org.gabmus.envision com.valvesoftware.Steam</tt>\n\n",
),
set_use_markup: true,
add_css_class: "warning",
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::selected_profile())"]
set_visible: !*IS_FLATPAK && !dep_pkexec().check(),
warning_heading(), warning_heading(),
gtk::Label { gtk::Label {
set_label: &format!( set_label: &format!(

View file

@ -1,4 +1,4 @@
use crate::{async_process::async_process, profile::Profile}; use crate::{async_process::async_process, is_flatpak::IS_FLATPAK, profile::Profile};
use anyhow::bail; use anyhow::bail;
use nix::{ use nix::{
errno::Errno, errno::Errno,
@ -81,7 +81,15 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
} }
pub async fn setcap_cap_sys_nice_eip(profile: &Profile) { pub async fn setcap_cap_sys_nice_eip(profile: &Profile) {
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None).await let setcap_cmd = setcap_cap_sys_nice_eip_cmd(profile);
if *IS_FLATPAK {
let mut flatpak_cmd = vec!["--host".into(), "pkexec".into()];
flatpak_cmd.extend(setcap_cmd);
if let Err(e) = async_process("flatpak-spawn", Some(&flatpak_cmd), None).await
{
eprintln!("Error: failed running setcap: {e}");
}
} else if let Err(e) = async_process("pkexec", Some(&setcap_cmd), None).await
{ {
eprintln!("Error: failed running setcap: {e}"); eprintln!("Error: failed running setcap: {e}");
} }