forked from lix-project/lix-installer
Support for SteamOS Nix Offload in SteamOS 20230522.1000 (#495)
* Document how to handle different branches and buildIDs of the steam deck OS * Changes required for SteamOS 20230522.1000 * Speeling * Handle steamos upgrades better * Speeling * Tidy
This commit is contained in:
parent
539c21ec28
commit
5a07b2331b
|
@ -165,7 +165,7 @@ impl Action for CreateDirectory {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
create_dir(path.clone())
|
create_dir(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e))
|
.map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e))
|
||||||
.map_err(Self::error)?;
|
.map_err(Self::error)?;
|
||||||
|
|
102
src/action/linux/ensure_steamos_nix_directory.rs
Normal file
102
src/action/linux/ensure_steamos_nix_directory.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use tokio::fs::create_dir;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tracing::{span, Span};
|
||||||
|
|
||||||
|
use crate::action::{ActionError, ActionErrorKind, ActionTag};
|
||||||
|
use crate::execute_command;
|
||||||
|
|
||||||
|
use crate::action::{Action, ActionDescription, StatefulAction};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Ensure SeamOS's `/nix` folder exists.
|
||||||
|
|
||||||
|
In SteamOS build ID 20230522.1000 (and, presumably, later) a `/nix` directory and related units
|
||||||
|
exist. In previous versions of `nix-installer` the uninstall process would remove that directory.
|
||||||
|
This action ensures that the folder does indeed exist.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||||
|
pub struct EnsureSteamosNixDirectory;
|
||||||
|
|
||||||
|
impl EnsureSteamosNixDirectory {
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn plan() -> Result<StatefulAction<Self>, ActionError> {
|
||||||
|
if which::which("steamos-readonly").is_err() {
|
||||||
|
return Err(Self::error(ActionErrorKind::MissingSteamosBinary(
|
||||||
|
"steamos-readonly".into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if Path::new("/nix").exists() {
|
||||||
|
Ok(StatefulAction::completed(EnsureSteamosNixDirectory))
|
||||||
|
} else {
|
||||||
|
Ok(StatefulAction::uncompleted(EnsureSteamosNixDirectory))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
#[typetag::serde(name = "ensure_steamos_nix_directory")]
|
||||||
|
impl Action for EnsureSteamosNixDirectory {
|
||||||
|
fn action_tag() -> ActionTag {
|
||||||
|
ActionTag("ensure_steamos_nix_directory")
|
||||||
|
}
|
||||||
|
fn tracing_synopsis(&self) -> String {
|
||||||
|
format!("Ensure SteamOS's `/nix` directory exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracing_span(&self) -> Span {
|
||||||
|
span!(tracing::Level::DEBUG, "ensure_steamos_nix_directory",)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_description(&self) -> Vec<ActionDescription> {
|
||||||
|
vec![ActionDescription::new(
|
||||||
|
self.tracing_synopsis(),
|
||||||
|
vec![
|
||||||
|
"On more recent versions of SteamOS, a `/nix` folder now exists on the base image.".to_string(),
|
||||||
|
"Previously, `nix-installer` created this directory through systemd units.".to_string(),
|
||||||
|
"It's likely you updated SteamOS, then ran `/nix/nix-installer uninstall`, which deleted the `/nix` directory.".to_string(),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
async fn execute(&mut self) -> Result<(), ActionError> {
|
||||||
|
execute_command(
|
||||||
|
Command::new("steamos-readonly")
|
||||||
|
.process_group(0)
|
||||||
|
.arg("disable")
|
||||||
|
.stdin(std::process::Stdio::null()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Self::error)?;
|
||||||
|
|
||||||
|
let path = PathBuf::from("/nix");
|
||||||
|
create_dir(&path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ActionErrorKind::CreateDirectory(path.clone(), e))
|
||||||
|
.map_err(Self::error)?;
|
||||||
|
|
||||||
|
execute_command(
|
||||||
|
Command::new("steamos-readonly")
|
||||||
|
.process_group(0)
|
||||||
|
.arg("enable")
|
||||||
|
.stdin(std::process::Stdio::null()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Self::error)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||||
|
// noop
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
|
pub(crate) mod ensure_steamos_nix_directory;
|
||||||
pub(crate) mod provision_selinux;
|
pub(crate) mod provision_selinux;
|
||||||
pub(crate) mod start_systemd_unit;
|
pub(crate) mod start_systemd_unit;
|
||||||
|
pub(crate) mod systemctl_daemon_reload;
|
||||||
|
|
||||||
|
pub use ensure_steamos_nix_directory::EnsureSteamosNixDirectory;
|
||||||
pub use provision_selinux::ProvisionSelinux;
|
pub use provision_selinux::ProvisionSelinux;
|
||||||
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};
|
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};
|
||||||
|
pub use systemctl_daemon_reload::SystemctlDaemonReload;
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl Action for StartSystemdUnit {
|
||||||
ActionTag("start_systemd_unit")
|
ActionTag("start_systemd_unit")
|
||||||
}
|
}
|
||||||
fn tracing_synopsis(&self) -> String {
|
fn tracing_synopsis(&self) -> String {
|
||||||
format!("Enable (and start) the systemd unit {}", self.unit)
|
format!("Enable (and start) the systemd unit `{}`", self.unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracing_span(&self) -> Span {
|
fn tracing_span(&self) -> Span {
|
||||||
|
@ -106,7 +106,7 @@ impl Action for StartSystemdUnit {
|
||||||
|
|
||||||
fn revert_description(&self) -> Vec<ActionDescription> {
|
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||||
vec![ActionDescription::new(
|
vec![ActionDescription::new(
|
||||||
format!("Disable (and stop) the systemd unit {}", self.unit),
|
format!("Disable (and stop) the systemd unit `{}`", self.unit),
|
||||||
vec![],
|
vec![],
|
||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
81
src/action/linux/systemctl_daemon_reload.rs
Normal file
81
src/action/linux/systemctl_daemon_reload.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tracing::{span, Span};
|
||||||
|
|
||||||
|
use crate::action::{ActionError, ActionErrorKind, ActionTag};
|
||||||
|
use crate::execute_command;
|
||||||
|
|
||||||
|
use crate::action::{Action, ActionDescription, StatefulAction};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Run `systemctl daemon-reload` (on both execute and revert)
|
||||||
|
*/
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||||
|
pub struct SystemctlDaemonReload;
|
||||||
|
|
||||||
|
impl SystemctlDaemonReload {
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn plan() -> Result<StatefulAction<Self>, ActionError> {
|
||||||
|
if !Path::new("/run/systemd/system").exists() {
|
||||||
|
return Err(Self::error(ActionErrorKind::SystemdMissing));
|
||||||
|
}
|
||||||
|
|
||||||
|
if which::which("systemctl").is_err() {
|
||||||
|
return Err(Self::error(ActionErrorKind::SystemdMissing));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(StatefulAction::uncompleted(SystemctlDaemonReload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
#[typetag::serde(name = "systemctl_daemon_reload")]
|
||||||
|
impl Action for SystemctlDaemonReload {
|
||||||
|
fn action_tag() -> ActionTag {
|
||||||
|
ActionTag("systemctl_daemon_reload")
|
||||||
|
}
|
||||||
|
fn tracing_synopsis(&self) -> String {
|
||||||
|
format!("Run `systemctl daemon-reload`")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracing_span(&self) -> Span {
|
||||||
|
span!(tracing::Level::DEBUG, "systemctl_daemon_reload",)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_description(&self) -> Vec<ActionDescription> {
|
||||||
|
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
async fn execute(&mut self) -> Result<(), ActionError> {
|
||||||
|
execute_command(
|
||||||
|
Command::new("systemctl")
|
||||||
|
.process_group(0)
|
||||||
|
.arg("daemon-reload")
|
||||||
|
.stdin(std::process::Stdio::null()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Self::error)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||||
|
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||||
|
execute_command(
|
||||||
|
Command::new("systemctl")
|
||||||
|
.process_group(0)
|
||||||
|
.arg("daemon-reload")
|
||||||
|
.stdin(std::process::Stdio::null()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Self::error)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -243,7 +243,7 @@ pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
|
||||||
///
|
///
|
||||||
/// If this action calls sub-[`Action`]s, care should be taken to call [`try_revert`][StatefulAction::try_revert], not [`revert`][Action::revert], so that [`ActionState`] is handled correctly and tracing is done.
|
/// If this action calls sub-[`Action`]s, care should be taken to call [`try_revert`][StatefulAction::try_revert], not [`revert`][Action::revert], so that [`ActionState`] is handled correctly and tracing is done.
|
||||||
///
|
///
|
||||||
/// /// This is called by [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) through [`StatefulAction::try_revert`] which handles tracing as well as if the action needs to revert based on its `action_state`.
|
/// This is called by [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) through [`StatefulAction::try_revert`] which handles tracing as well as if the action needs to revert based on its `action_state`.
|
||||||
async fn revert(&mut self) -> Result<(), ActionError>;
|
async fn revert(&mut self) -> Result<(), ActionError>;
|
||||||
|
|
||||||
fn stateful(self) -> StatefulAction<Self>
|
fn stateful(self) -> StatefulAction<Self>
|
||||||
|
@ -518,6 +518,8 @@ pub enum ActionErrorKind {
|
||||||
Plist(#[from] plist::Error),
|
Plist(#[from] plist::Error),
|
||||||
#[error("Unexpected binary tarball contents found, the build result from `https://releases.nixos.org/?prefix=nix/` or `nix build nix#hydraJobs.binaryTarball.$SYSTEM` is expected")]
|
#[error("Unexpected binary tarball contents found, the build result from `https://releases.nixos.org/?prefix=nix/` or `nix build nix#hydraJobs.binaryTarball.$SYSTEM` is expected")]
|
||||||
MalformedBinaryTarball,
|
MalformedBinaryTarball,
|
||||||
|
#[error("Could not find `{0}` in PATH; This action only works on SteamOS, which should have this present in PATH.")]
|
||||||
|
MissingSteamosBinary(String),
|
||||||
#[error(
|
#[error(
|
||||||
"Could not find a supported command to create users in PATH; please install `useradd` or `adduser`"
|
"Could not find a supported command to create users in PATH; please install `useradd` or `adduser`"
|
||||||
)]
|
)]
|
||||||
|
|
|
@ -381,6 +381,9 @@ pub enum PlannerError {
|
||||||
NixExists,
|
NixExists,
|
||||||
#[error("WSL1 is not supported, please upgrade to WSL2: https://learn.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2")]
|
#[error("WSL1 is not supported, please upgrade to WSL2: https://learn.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2")]
|
||||||
Wsl1,
|
Wsl1,
|
||||||
|
/// Failed to execute command
|
||||||
|
#[error("Failed to execute command `{0}`")]
|
||||||
|
Command(String, #[source] std::io::Error),
|
||||||
#[cfg(feature = "diagnostics")]
|
#[cfg(feature = "diagnostics")]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Diagnostic(#[from] crate::diagnostics::DiagnosticError),
|
Diagnostic(#[from] crate::diagnostics::DiagnosticError),
|
||||||
|
@ -407,6 +410,7 @@ impl HasExpectedErrors for PlannerError {
|
||||||
this @ PlannerError::NixOs => Some(Box::new(this)),
|
this @ PlannerError::NixOs => Some(Box::new(this)),
|
||||||
this @ PlannerError::NixExists => Some(Box::new(this)),
|
this @ PlannerError::NixExists => Some(Box::new(this)),
|
||||||
this @ PlannerError::Wsl1 => Some(Box::new(this)),
|
this @ PlannerError::Wsl1 => Some(Box::new(this)),
|
||||||
|
PlannerError::Command(_, _) => None,
|
||||||
#[cfg(feature = "diagnostics")]
|
#[cfg(feature = "diagnostics")]
|
||||||
PlannerError::Diagnostic(diagnostic_error) => Some(Box::new(diagnostic_error)),
|
PlannerError::Diagnostic(diagnostic_error) => Some(Box::new(diagnostic_error)),
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ One time step:
|
||||||
6. Run `sudo steamos-chroot --disk /dev/nvme0n1 --partset B` and inside run the same above commands
|
6. Run `sudo steamos-chroot --disk /dev/nvme0n1 --partset B` and inside run the same above commands
|
||||||
7. Safely turn off the VM!
|
7. Safely turn off the VM!
|
||||||
|
|
||||||
|
|
||||||
Repeated step:
|
Repeated step:
|
||||||
1. Create a snapshot of the base install to work on
|
1. Create a snapshot of the base install to work on
|
||||||
```sh
|
```sh
|
||||||
|
@ -58,14 +59,52 @@ Repeated step:
|
||||||
```
|
```
|
||||||
3. **Do your testing!** You can `ssh deck@localhost -p 2222` in and use `rsync -e 'ssh -p 2222' result/bin/nix-installer deck@localhost:nix-installer` to send a `nix-installer build.
|
3. **Do your testing!** You can `ssh deck@localhost -p 2222` in and use `rsync -e 'ssh -p 2222' result/bin/nix-installer deck@localhost:nix-installer` to send a `nix-installer build.
|
||||||
4. Delete `steamos-hack.qcow2`
|
4. Delete `steamos-hack.qcow2`
|
||||||
|
|
||||||
|
|
||||||
|
To test a specific channel of the Steam Deck:
|
||||||
|
1. Use `steamos-select-branch -l` to list possible branches.
|
||||||
|
2. Run `steamos-select-branch $BRANCH` to choose a branch
|
||||||
|
3. Run `steamos-update`
|
||||||
|
4. Run `sudo steamos-chroot --disk /dev/vda --partset A` and inside run this
|
||||||
|
```sh
|
||||||
|
steamos-readonly disable
|
||||||
|
echo -e '[Autologin]\nSession=plasma.desktop' > /etc/sddm.conf.d/zz-steamos-autologin.conf
|
||||||
|
passwd deck
|
||||||
|
sudo systemctl enable sshd
|
||||||
|
steamos-readonly enable
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
5. Run `sudo steamos-chroot --disk /dev/vda --partset B` and inside run the same above commands
|
||||||
|
6. Safely turn off the VM!
|
||||||
|
|
||||||
|
|
||||||
|
To test on a specific build id of the Steam Deck:
|
||||||
|
1. Determine the build id to be targeted. On a running system this is found in `/etc/os-release` under `BUILD_ID`.
|
||||||
|
2. Run `steamos-update-os now --update-version $BUILD_ID`
|
||||||
|
+ If you can't access a specific build ID you may need to change branches, see above.
|
||||||
|
+ Be patient, don't ctrl+C it, it breaks. Don't reboot yet!
|
||||||
|
4. Run `sudo steamos-chroot --disk /dev/vda --partset A` and inside run this
|
||||||
|
```sh
|
||||||
|
steamos-readonly disable
|
||||||
|
echo -e '[Autologin]\nSession=plasma.desktop' > /etc/sddm.conf.d/zz-steamos-autologin.conf
|
||||||
|
passwd deck
|
||||||
|
sudo systemctl enable sshd
|
||||||
|
steamos-readonly enable
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
5. Run `sudo steamos-chroot --disk /dev/vda --partset B` and inside run the same above commands
|
||||||
|
6. Safely turn off the VM!
|
||||||
|
|
||||||
*/
|
*/
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf, process::Output};
|
||||||
|
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
action::{
|
action::{
|
||||||
base::{CreateDirectory, CreateFile, RemoveDirectory},
|
base::{CreateDirectory, CreateFile, RemoveDirectory},
|
||||||
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
|
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
|
||||||
linux::StartSystemdUnit,
|
linux::{EnsureSteamosNixDirectory, StartSystemdUnit, SystemctlDaemonReload},
|
||||||
Action, StatefulAction,
|
Action, StatefulAction,
|
||||||
},
|
},
|
||||||
planner::{Planner, PlannerError},
|
planner::{Planner, PlannerError},
|
||||||
|
@ -78,6 +117,7 @@ use super::ShellProfileLocations;
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[cfg_attr(feature = "cli", derive(clap::Parser))]
|
#[cfg_attr(feature = "cli", derive(clap::Parser))]
|
||||||
pub struct SteamDeck {
|
pub struct SteamDeck {
|
||||||
|
/// Where `/nix` will be bind mounted to. Deprecated in SteamOS build ID 20230522.1000 or later
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "cli",
|
feature = "cli",
|
||||||
clap(
|
clap(
|
||||||
|
@ -102,89 +142,127 @@ impl Planner for SteamDeck {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
|
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
|
||||||
let persistence = &self.persistence;
|
// Starting in roughly build ID `20230522.1000`, the Steam Deck has a `/home/.steamos/offload/nix` directory and `nix.mount` unit we can use instead of creating a mountpoint.
|
||||||
if !persistence.is_absolute() {
|
let requires_nix_bind_mount = detect_requires_bind_mount().await?;
|
||||||
return Err(PlannerError::Custom(Box::new(
|
|
||||||
SteamDeckError::AbsolutePathRequired(self.persistence.clone()),
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let nix_directory_buf = format!(
|
let mut actions = vec![
|
||||||
"\
|
// Primarily for uninstall
|
||||||
[Unit]\n\
|
SystemctlDaemonReload::plan()
|
||||||
Description=Create a `/nix` directory to be used for bind mounting\n\
|
.await
|
||||||
PropagatesStopTo=nix-daemon.service\n\
|
.map_err(PlannerError::Action)?
|
||||||
PropagatesStopTo=nix.mount\n\
|
.boxed(),
|
||||||
DefaultDependencies=no\n\
|
];
|
||||||
After=grub-recordfail.service\n\
|
|
||||||
After=steamos-finish-oobe-migration.service\n\
|
|
||||||
\n\
|
|
||||||
[Service]\n\
|
|
||||||
Type=oneshot\n\
|
|
||||||
ExecStart=steamos-readonly disable\n\
|
|
||||||
ExecStart=mkdir -vp /nix\n\
|
|
||||||
ExecStart=chmod -v 0755 /nix\n\
|
|
||||||
ExecStart=chown -v root /nix\n\
|
|
||||||
ExecStart=chgrp -v root /nix\n\
|
|
||||||
ExecStart=steamos-readonly enable\n\
|
|
||||||
ExecStop=steamos-readonly disable\n\
|
|
||||||
ExecStop=rmdir /nix\n\
|
|
||||||
ExecStop=steamos-readonly enable\n\
|
|
||||||
RemainAfterExit=true\n\
|
|
||||||
"
|
|
||||||
);
|
|
||||||
let nix_directory_unit = CreateFile::plan(
|
|
||||||
"/etc/systemd/system/nix-directory.service",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
0o0644,
|
|
||||||
nix_directory_buf,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(PlannerError::Action)?;
|
|
||||||
|
|
||||||
let create_bind_mount_buf = format!(
|
if let Ok(nix_mount_status) = systemctl_status("nix.mount").await {
|
||||||
"\
|
let nix_mount_status_stderr = String::from_utf8(nix_mount_status.stderr)?;
|
||||||
[Unit]\n\
|
if nix_mount_status_stderr.contains("Warning: The unit file, source configuration file or drop-ins of nix.mount changed on disk. Run 'systemctl daemon-reload' to reload units.") {
|
||||||
Description=Mount `{persistence}` on `/nix`\n\
|
return Err(PlannerError::Custom(Box::new(
|
||||||
PropagatesStopTo=nix-daemon.service\n\
|
SteamDeckError::NixMountSystemctlDaemonReloadRequired,
|
||||||
PropagatesStopTo=nix-directory.service\n\
|
)))
|
||||||
After=nix-directory.service\n\
|
}
|
||||||
Requires=nix-directory.service\n\
|
}
|
||||||
ConditionPathIsDirectory=/nix\n\
|
|
||||||
DefaultDependencies=no\n\
|
if requires_nix_bind_mount {
|
||||||
\n\
|
let persistence = &self.persistence;
|
||||||
[Mount]\n\
|
if !persistence.is_absolute() {
|
||||||
What={persistence}\n\
|
return Err(PlannerError::Custom(Box::new(
|
||||||
Where=/nix\n\
|
SteamDeckError::AbsolutePathRequired(self.persistence.clone()),
|
||||||
Type=none\n\
|
)));
|
||||||
DirectoryMode=0755\n\
|
};
|
||||||
Options=bind\n\
|
actions.push(
|
||||||
\n\
|
CreateDirectory::plan(&persistence, None, None, 0o0755, true)
|
||||||
[Install]\n\
|
.await
|
||||||
RequiredBy=nix-daemon.service\n\
|
.map_err(PlannerError::Action)?
|
||||||
RequiredBy=nix-daemon.socket\n
|
.boxed(),
|
||||||
",
|
);
|
||||||
persistence = persistence.display(),
|
|
||||||
);
|
let nix_directory_buf = format!(
|
||||||
let create_bind_mount_unit = CreateFile::plan(
|
"\
|
||||||
"/etc/systemd/system/nix.mount",
|
[Unit]\n\
|
||||||
None,
|
Description=Create a `/nix` directory to be used for bind mounting\n\
|
||||||
None,
|
PropagatesStopTo=nix-daemon.service\n\
|
||||||
0o0644,
|
PropagatesStopTo=nix.mount\n\
|
||||||
create_bind_mount_buf,
|
DefaultDependencies=no\n\
|
||||||
false,
|
After=grub-recordfail.service\n\
|
||||||
)
|
After=steamos-finish-oobe-migration.service\n\
|
||||||
.await
|
\n\
|
||||||
.map_err(PlannerError::Action)?;
|
[Service]\n\
|
||||||
|
Type=oneshot\n\
|
||||||
|
ExecStart=steamos-readonly disable\n\
|
||||||
|
ExecStart=mkdir -vp /nix\n\
|
||||||
|
ExecStart=chmod -v 0755 /nix\n\
|
||||||
|
ExecStart=chown -v root /nix\n\
|
||||||
|
ExecStart=chgrp -v root /nix\n\
|
||||||
|
ExecStart=steamos-readonly enable\n\
|
||||||
|
ExecStop=steamos-readonly disable\n\
|
||||||
|
ExecStop=rmdir /nix\n\
|
||||||
|
ExecStop=steamos-readonly enable\n\
|
||||||
|
RemainAfterExit=true\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
let nix_directory_unit = CreateFile::plan(
|
||||||
|
"/etc/systemd/system/nix-directory.service",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0o0644,
|
||||||
|
nix_directory_buf,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?;
|
||||||
|
actions.push(nix_directory_unit.boxed());
|
||||||
|
|
||||||
|
let create_bind_mount_buf = format!(
|
||||||
|
"\
|
||||||
|
[Unit]\n\
|
||||||
|
Description=Mount `{persistence}` on `/nix`\n\
|
||||||
|
PropagatesStopTo=nix-daemon.service\n\
|
||||||
|
PropagatesStopTo=nix-directory.service\n\
|
||||||
|
After=nix-directory.service\n\
|
||||||
|
Requires=nix-directory.service\n\
|
||||||
|
ConditionPathIsDirectory=/nix\n\
|
||||||
|
DefaultDependencies=no\n\
|
||||||
|
\n\
|
||||||
|
[Mount]\n\
|
||||||
|
What={persistence}\n\
|
||||||
|
Where=/nix\n\
|
||||||
|
Type=none\n\
|
||||||
|
DirectoryMode=0755\n\
|
||||||
|
Options=bind\n\
|
||||||
|
\n\
|
||||||
|
[Install]\n\
|
||||||
|
RequiredBy=nix-daemon.service\n\
|
||||||
|
RequiredBy=nix-daemon.socket\n
|
||||||
|
",
|
||||||
|
persistence = persistence.display(),
|
||||||
|
);
|
||||||
|
let create_bind_mount_unit = CreateFile::plan(
|
||||||
|
"/etc/systemd/system/nix.mount",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0o0644,
|
||||||
|
create_bind_mount_buf,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?;
|
||||||
|
actions.push(create_bind_mount_unit.boxed());
|
||||||
|
} else {
|
||||||
|
let ensure_steamos_nix_directory = EnsureSteamosNixDirectory::plan()
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?;
|
||||||
|
actions.push(ensure_steamos_nix_directory.boxed());
|
||||||
|
let start_nix_mount = StartSystemdUnit::plan("nix.mount".to_string(), true)
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?;
|
||||||
|
actions.push(start_nix_mount.boxed());
|
||||||
|
}
|
||||||
|
|
||||||
let ensure_symlinked_units_resolve_buf = format!(
|
let ensure_symlinked_units_resolve_buf = format!(
|
||||||
"\
|
"\
|
||||||
[Unit]\n\
|
[Unit]\n\
|
||||||
Description=Ensure Nix related units which are symlinked resolve\n\
|
Description=Ensure Nix related units which are symlinked resolve\n\
|
||||||
After=nix.mount\n\
|
After=nix.mount\n\
|
||||||
Requires=nix-directory.service\n\
|
|
||||||
Requires=nix.mount\n\
|
Requires=nix.mount\n\
|
||||||
DefaultDependencies=no\n\
|
DefaultDependencies=no\n\
|
||||||
\n\
|
\n\
|
||||||
|
@ -208,6 +286,7 @@ impl Planner for SteamDeck {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(PlannerError::Action)?;
|
.map_err(PlannerError::Action)?;
|
||||||
|
actions.push(ensure_symlinked_units_resolve_unit.boxed());
|
||||||
|
|
||||||
// We need to remove this path since it's part of the read-only install.
|
// We need to remove this path since it's part of the read-only install.
|
||||||
let mut shell_profile_locations = ShellProfileLocations::default();
|
let mut shell_profile_locations = ShellProfileLocations::default();
|
||||||
|
@ -223,18 +302,16 @@ impl Planner for SteamDeck {
|
||||||
.remove(index);
|
.remove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![
|
if requires_nix_bind_mount {
|
||||||
CreateDirectory::plan(&persistence, None, None, 0o0755, true)
|
actions.push(
|
||||||
.await
|
StartSystemdUnit::plan("nix.mount".to_string(), false)
|
||||||
.map_err(PlannerError::Action)?
|
.await
|
||||||
.boxed(),
|
.map_err(PlannerError::Action)?
|
||||||
nix_directory_unit.boxed(),
|
.boxed(),
|
||||||
create_bind_mount_unit.boxed(),
|
)
|
||||||
ensure_symlinked_units_resolve_unit.boxed(),
|
}
|
||||||
StartSystemdUnit::plan("nix.mount".to_string(), false)
|
|
||||||
.await
|
actions.append(&mut vec![
|
||||||
.map_err(PlannerError::Action)?
|
|
||||||
.boxed(),
|
|
||||||
ProvisionNix::plan(&self.settings.clone())
|
ProvisionNix::plan(&self.settings.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(PlannerError::Action)?
|
.map_err(PlannerError::Action)?
|
||||||
|
@ -260,7 +337,12 @@ impl Planner for SteamDeck {
|
||||||
.await
|
.await
|
||||||
.map_err(PlannerError::Action)?
|
.map_err(PlannerError::Action)?
|
||||||
.boxed(),
|
.boxed(),
|
||||||
])
|
SystemctlDaemonReload::plan()
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?
|
||||||
|
.boxed(),
|
||||||
|
]);
|
||||||
|
Ok(actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
|
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
|
||||||
|
@ -320,4 +402,36 @@ impl Into<BuiltinPlanner> for SteamDeck {
|
||||||
pub enum SteamDeckError {
|
pub enum SteamDeckError {
|
||||||
#[error("`{0}` is not a path that can be canonicalized into an absolute path, bind mounts require an absolute path")]
|
#[error("`{0}` is not a path that can be canonicalized into an absolute path, bind mounts require an absolute path")]
|
||||||
AbsolutePathRequired(PathBuf),
|
AbsolutePathRequired(PathBuf),
|
||||||
|
#[error("A `/home/.steamos/offload/nix` exists, however `nix.mount` does not point at it. If Nix was previously installed, try uninstalling then rebooting first")]
|
||||||
|
OffloadExistsButUnitIncorrect,
|
||||||
|
#[error("Detected the SteamOS `nix.mount` unit exists, but `systemctl status nix.mount` did not return success. Try running `systemctl daemon-reload`.")]
|
||||||
|
SteamosNixMountUnitNotExists,
|
||||||
|
#[error("Detected the SteamOS `nix.mount` unit exists, but `systemctl status nix.mount` returned a warning that `systemctl daemon-reload` should be run. Run `systemctl daemon-reload` then `systemctl start nix.mount`, then try again.")]
|
||||||
|
NixMountSystemctlDaemonReloadRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn detect_requires_bind_mount() -> Result<bool, PlannerError> {
|
||||||
|
let steamos_nix_mount_unit_path = "/usr/lib/systemd/system/nix.mount";
|
||||||
|
let nix_mount_unit = tokio::fs::read_to_string(steamos_nix_mount_unit_path)
|
||||||
|
.await
|
||||||
|
.map(|v| Some(v))
|
||||||
|
.unwrap_or_else(|_| None);
|
||||||
|
|
||||||
|
match nix_mount_unit {
|
||||||
|
Some(nix_mount_unit) if nix_mount_unit.contains("What=/home/.steamos/offload/nix") => {
|
||||||
|
Ok(false)
|
||||||
|
},
|
||||||
|
None | Some(_) => Ok(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn systemctl_status(unit: &str) -> Result<Output, PlannerError> {
|
||||||
|
let mut command = Command::new("systemctl");
|
||||||
|
command.arg("status");
|
||||||
|
command.arg(unit);
|
||||||
|
let output = command
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| PlannerError::Command(format!("{:?}", command.as_std()), e))?;
|
||||||
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue