Merge branch 'flatpak-packaging' into 'main'

Draft: feat: flatpak packaging

See merge request gabmus/envision!90
This commit is contained in:
Charlie Le 2024-11-25 20:08:21 +00:00
commit c99c0eacb3
22 changed files with 273 additions and 72 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

@ -2,6 +2,7 @@ use std::ffi::OsStr;
use std::process::Stdio;
use std::{collections::HashMap, os::unix::process::ExitStatusExt};
use tokio::process::Command;
use crate::is_flatpak::IS_FLATPAK;
pub struct AsyncProcessOut {
pub exit_code: i32,
@ -13,10 +14,22 @@ pub async fn async_process<S: AsRef<OsStr>, T: AsRef<OsStr>>(
cmd: S,
args: Option<&[T]>,
env: Option<HashMap<String, String>>,
host_spawn: bool,
) -> anyhow::Result<AsyncProcessOut> {
let cmd = Command::new(cmd)
.args(args.unwrap_or_default())
.envs(env.unwrap_or_default())
let mut command: Command;
if *IS_FLATPAK && host_spawn {
command = Command::new("flatpak-spawn");
command.arg("--host");
for (key, value) in env.unwrap_or_default() {
command.arg(format!("--env={}={}", key, value));
}
command.arg(cmd).args(args.unwrap_or_default());
} else {
command = Command::new(cmd);
command.args(args.unwrap_or_default()).envs(env.unwrap_or_default());
}
let cmd = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())

View file

@ -26,7 +26,7 @@ impl Cmake {
}
}
args.push(self.source_dir.to_string_lossy().to_string());
WorkerJob::new_cmd(self.env.clone(), "cmake".into(), Some(args))
WorkerJob::new_cmd(self.env.clone(), "cmake".into(), Some(args), false)
}
pub fn get_build_job(&self) -> WorkerJob {
@ -37,6 +37,7 @@ impl Cmake {
"--build".into(),
self.build_dir.to_string_lossy().to_string(),
]),
false,
)
}
@ -48,6 +49,7 @@ impl Cmake {
"--install".into(),
self.build_dir.to_string_lossy().to_string(),
]),
false,
)
}
}

View file

@ -13,7 +13,7 @@ impl Git {
fn cmd(&self, args: Vec<String>) -> WorkerJob {
let mut nargs = vec!["-C".into(), self.dir.to_string_lossy().to_string()];
nargs.extend(args);
WorkerJob::new_cmd(None, "git".into(), Some(nargs))
WorkerJob::new_cmd(None, "git".into(), Some(nargs), false)
}
fn get_repo(&self) -> String {
@ -84,6 +84,7 @@ impl Git {
self.dir.to_string_lossy().to_string(),
"--recurse-submodules".into(),
]),
false,
)
}

View file

@ -79,6 +79,7 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
.to_string_lossy()
.to_string(),
]),
false,
));
jobs.push_back(WorkerJob::new_cmd(
None,
@ -100,6 +101,7 @@ pub fn get_build_basalt_jobs(profile: &Profile, clean_build: bool) -> VecDeque<W
.to_string_lossy()
.to_string(),
]),
false,
));
jobs

View file

@ -21,6 +21,7 @@ pub fn get_build_mercury_jobs(profile: &Profile) -> VecDeque<WorkerJob> {
profile.prefix.to_string_lossy().to_string(),
get_cache_dir().to_string_lossy().to_string(),
]),
false,
));
jobs

View file

@ -62,6 +62,7 @@ pub fn get_build_openhmd_jobs(profile: &Profile, clean_build: bool) -> VecDeque<
.to_string_lossy()
.to_string(),
]),
false,
));
}
// build job
@ -69,6 +70,7 @@ pub fn get_build_openhmd_jobs(profile: &Profile, clean_build: bool) -> VecDeque<
None,
"ninja".into(),
Some(vec!["-C".into(), build_dir.to_string_lossy().to_string()]),
false,
));
// install job
jobs.push_back(WorkerJob::new_cmd(
@ -79,6 +81,7 @@ pub fn get_build_openhmd_jobs(profile: &Profile, clean_build: bool) -> VecDeque<
build_dir.to_string_lossy().to_string(),
"install".into(),
]),
false,
));
jobs

View file

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

View file

@ -1,8 +1,5 @@
use crate::{
paths::{get_backup_dir, SYSTEM_PREFIX},
profile::Profile,
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
xdg::XDG,
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
};
use serde::{Deserialize, Serialize};
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<()> {
let dest = get_active_runtime_json_path();
set_file_readonly(&dest, false)?;
backup_steam_active_runtime();
let pfx = profile.clone().prefix;
let mut ar = build_profile_active_runtime(profile)?;
// hack: relativize libopenxr_monado.so path for system installs
if pfx == PathBuf::from(SYSTEM_PREFIX) {
ar = relativize_active_runtime_lib_path(&ar, &dest);
let mut dests = vec![
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();
let pfx: PathBuf = profile.clone().prefix;
for dest in dests {
if !dest.is_file() {
continue;
}
let mut ar = build_profile_active_runtime(profile)?;
// hack: relativize libopenxr_monado.so path for system installs
if pfx == PathBuf::from(SYSTEM_PREFIX) {
ar = relativize_active_runtime_lib_path(&ar, &dest);
}
set_file_readonly(&dest, false)?;
dump_active_runtime_to_path(&ar, &dest)?;
set_file_readonly(&dest, true)?;
}
dump_current_active_runtime(&ar)?;
set_file_readonly(&dest, true)?;
Ok(())
}

View file

@ -1,10 +1,7 @@
use std::path::{Path, PathBuf};
use crate::{
paths::get_backup_dir,
profile::Profile,
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
xdg::XDG,
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
};
use serde::{ser::Error, Deserialize, Serialize};
@ -23,9 +20,19 @@ pub fn get_openvr_conf_dir() -> 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")
}
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 {
ovr_paths.runtime.iter().any(|rt| {
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<()> {
let dest = get_openvrpaths_vrpath_path();
set_file_readonly(&dest, false)?;
backup_steam_openvrpaths();
dump_current_openvrpaths(&build_profile_openvrpaths(profile))?;
set_file_readonly(&dest, true)?;
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)?;
}
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 gpu_profile;
pub mod is_appimage;
pub mod is_flatpak;
pub mod linux_distro;
pub mod log_parser;
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_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_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,
libsurvive_deps::get_missing_libsurvive_deps, mercury_deps::get_missing_mercury_deps,
monado_deps::get_missing_monado_deps, openhmd_deps::get_missing_openhmd_deps,
wivrn_deps::get_missing_wivrn_deps,
},
file_builders::{
}, file_builders::{
active_runtime_json::{
set_current_active_runtime_to_profile, set_current_active_runtime_to_steam,
},
openvrpaths_vrpath::{
set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam,
},
},
linux_distro::LinuxDistro,
openxr_prober::is_openxr_ready,
paths::get_data_dir,
profile::{Profile, XRServiceType},
stateless_action,
steam_linux_runtime_injector::{
}, 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::{
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::{cmd_utils::CommandUnsandbox, file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd}}, vulkaninfo::VulkanInfo, xr_devices::XRDevice
};
use adw::{prelude::*, ResponseAppearance};
use gtk::glib::{self, clone};
@ -56,7 +43,7 @@ use relm4::{
new_action_group, new_stateful_action, new_stateless_action,
prelude::*,
};
use std::{collections::VecDeque, fs::remove_file, time::Duration};
use std::{collections::VecDeque, fs::remove_file, process::Command, time::Duration};
pub struct App {
application: adw::Application,
@ -175,9 +162,16 @@ impl App {
};
self.debug_view.sender().emit(DebugViewMsg::ClearLog);
self.xr_devices = vec![];
remove_file(prof.xrservice_type.ipc_file_path())
.is_err()
.then(|| println!("Failed to remove xrservice IPC file"));
if *IS_FLATPAK {
Command::new_unsandboxed("rm", None)
.arg(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())
.is_err()
.then(|| println!("Failed to remove xrservice IPC file"));
}
let worker = JobWorker::xrservice_worker_wrap_from_profile(
&prof,
sender.input_sender(),
@ -222,6 +216,7 @@ impl App {
Some(prof.environment.clone()),
"sh".into(),
Some(vec!["-c".into(), autostart_cmd.clone()]),
true
));
let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows),
@ -533,7 +528,7 @@ impl AsyncComponent for App {
.sender()
.emit(BuildWindowMsg::UpdateBuildStatus(BuildStatus::Done));
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));
} else {
alert_w_widget(
@ -606,11 +601,11 @@ impl AsyncComponent for App {
.emit(DebugViewMsg::UpdateSelectedProfile(prof.clone()));
}
Msg::RunSetCap => {
if !dep_pkexec().check() {
println!("pkexec not found, skipping setcap");
} else {
if dep_pkexec().check() || *IS_FLATPAK {
let profile = self.get_selected_profile();
setcap_cap_sys_nice_eip(&profile).await;
} else {
println!("pkexec not found, skipping setcap");
}
}
Msg::ProfileSelected(prof) => {
@ -698,7 +693,7 @@ impl AsyncComponent for App {
let worker = JobWorker::new_with_timer(
Duration::from_millis(500),
WorkerJob::new_func(Box::new(move || {
let ready = is_openxr_ready();
let ready = !*IS_FLATPAK && is_openxr_ready();
FuncWorkerOut {
success: ready,
..Default::default()

View file

@ -218,6 +218,7 @@ impl AsyncComponent for InstallWivrnBox {
"adb",
Some(&["install", "-r", "-g", apk_path.to_string_lossy().to_string().as_str()]),
None,
false,
)
.await
{

View file

@ -3,19 +3,12 @@ use super::{
state::JobWorkerState,
};
use crate::{
profile::{LighthouseDriver, Profile},
ui::SENDER_IO_ERR_MSG,
profile::{LighthouseDriver, Profile}, ui::SENDER_IO_ERR_MSG, util::cmd_utils::CommandUnsandbox
};
use nix::unistd::Pid;
use relm4::{prelude::*, Worker};
use std::{
collections::VecDeque,
io::{BufRead, BufReader},
mem,
os::unix::process::ExitStatusExt,
process::{Command, Stdio},
sync::{Arc, Mutex},
thread,
collections::VecDeque, io::{BufRead, BufReader}, mem, os::unix::process::ExitStatusExt, process::{Command, Stdio}, sync::{Arc, Mutex}, thread
};
macro_rules! logger_thread {
@ -92,9 +85,15 @@ impl Worker for InternalJobWorker {
match &mut job {
WorkerJob::Cmd(data) => {
let data = data.clone();
if let Ok(mut cmd) = Command::new(data.command)
let mut cmd: Command;
if data.host_spawn {
cmd = Command::new_unsandboxed(&data.command, Some(data.environment))
} else {
cmd = Command::new(&data.command);
cmd.envs(data.environment);
}
if let Ok(mut cmd) = cmd
.args(data.args)
.envs(data.environment)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
@ -221,12 +220,13 @@ impl InternalJobWorker {
),
};
let data = CmdWorkerData {
environment: env,
command,
args,
environment: env.clone(),
command: command.clone(),
args: args.clone(),
host_spawn: true,
};
let mut jobs = VecDeque::new();
jobs.push_back(WorkerJob::Cmd(data));
jobs.push_back(WorkerJob::Cmd(data));
Self::builder().detach_worker(JobWorkerInit { jobs, state })
}
}

View file

@ -7,6 +7,7 @@ pub struct CmdWorkerData {
pub environment: HashMap<String, String>,
pub command: String,
pub args: Vec<String>,
pub host_spawn: bool,
}
#[derive(Debug, Clone)]
@ -38,11 +39,13 @@ impl WorkerJob {
env: Option<HashMap<String, String>>,
cmd: String,
args: Option<Vec<String>>,
host_spawn: bool
) -> Self {
Self::Cmd(CmdWorkerData {
environment: env.unwrap_or_default(),
command: cmd,
args: args.unwrap_or_default(),
host_spawn,
})
}

View file

@ -17,6 +17,7 @@ use crate::{
config::Config,
depcheck::common::dep_pkexec,
gpu_profile::{get_amd_gpu_power_profile, GpuPowerProfile},
is_flatpak::IS_FLATPAK,
paths::{get_data_dir, get_home_dir},
profile::{LighthouseDriver, Profile, XRServiceType},
stateless_action,
@ -270,7 +271,7 @@ impl SimpleComponent for MainView {
add_css_class: "card",
add_css_class: "padded",
#[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,
Err(_) => {
eprintln!(
@ -302,7 +303,34 @@ impl SimpleComponent for MainView {
add_css_class: "card",
add_css_class: "padded",
#[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(),
gtk::Label {
set_label: &format!(

View file

@ -159,6 +159,7 @@ impl SimpleComponent for SteamVrCalibrationBox {
Some(env.clone()),
vrcmd.clone(),
Some(vec!["--pollposes".into()]),
false,
));
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(_) => Self::Input::NoOp,
@ -178,6 +179,7 @@ impl SimpleComponent for SteamVrCalibrationBox {
Some(env),
vrcmd,
Some(vec!["--resetroomsetup".into()]),
false,
));
JobWorker::new(jobs, sender.input_sender(), |msg| match msg {
JobWorkerOut::Log(_) => Self::Input::NoOp,

View file

@ -136,7 +136,8 @@ impl AsyncComponent for WivrnWiredStartBox {
"adb shell am start -a android.intent.action.VIEW -d \"wivrn+tcp://127.0.0.1\" $PACKAGE"
)
]),
None
None,
false,
).await {
Ok(out) if out.exit_code == 0 =>
StartClientStatus::Success

24
src/util/cmd_utils.rs Normal file
View file

@ -0,0 +1,24 @@
use std::{collections::HashMap, process::Command};
use crate::is_flatpak::IS_FLATPAK;
pub trait CommandUnsandbox {
fn new_unsandboxed(program: &str, envs: Option<HashMap<String, String>>) -> Self;
}
impl CommandUnsandbox for Command {
fn new_unsandboxed(program: &str, envs: Option<HashMap<String, String>>) -> Self {
if *IS_FLATPAK {
let mut cmd = Command::new("flatpak-spawn");
cmd.arg("--host");
for (key, value) in envs.unwrap_or_default() {
cmd.arg(format!("--env={}={}", key, value));
}
cmd.arg(program);
return cmd;
}
let mut cmd = Command::new(program);
cmd.envs(envs.unwrap_or_default());
cmd
}
}

View file

@ -81,7 +81,7 @@ pub fn setcap_cap_sys_nice_eip_cmd(profile: &Profile) -> Vec<String> {
}
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
if let Err(e) = async_process("pkexec", Some(&setcap_cap_sys_nice_eip_cmd(profile)), None, true).await
{
eprintln!("Error: failed running setcap: {e}");
}

View file

@ -1,3 +1,4 @@
pub mod file_utils;
pub mod hash;
pub mod steamvr_utils;
pub mod cmd_utils;