forked from lix-project/lix-installer
Add Steam Deck Support (#34)
* Init Steam Deck support * Improve systemd units * Handle stopping nix-daemon.service before stopping mount * Better handle being in a sysext * Add some install directions * Add a KDE autostart script * Tidy up uninstall * Use stop/disable instead of disable --now * Fixup a double-disable * Repair some defaults * Tidy up services * Make ci test steam deck planner * Delete bonus line * Use newer image * Get steam-deck working hopefully * Create steamos-readonly mock * Make stub of steamos-readonly * Use sudo for chmod * Attempt CI fix * Don't add deck group * A more clever method * We have a new method and the CI can be cleaned a bit * Brazenly disable sandbox on the deck ci * Extra-conf takes vec * Dump lsblk mount * An even more clever method * More debugging symbols * More debugging symbols * Even more debugging * probe for issues * Get specific with permissions and ownership (for ci) * We are now way overboard on debugging symbols * Specify permissions on created home stub * Allow specifying persistence * Cleanup debugging bits * Fixup persistence path * Work out some better linking in units * units don't need executable * Tidy * Delint * Remove a note from readme * Github actions seems to have build the wrong checkout? * Doctest repair * Don't create directory twice * Restore missing doc comments Co-authored-by: Cole Helbling <cole.e.helbling@outlook.com>
This commit is contained in:
parent
0e2f27713f
commit
ae37842a0d
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
|
@ -70,15 +70,52 @@ jobs:
|
|||
name: harmonic-x86_64-linux
|
||||
- name: Set executable
|
||||
run: chmod +x ./harmonic
|
||||
|
||||
- name: Initial install
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install linux-multi --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Test run
|
||||
- name: Initial uninstall (without a `nix run` first)
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic uninstall --no-confirm
|
||||
|
||||
- name: Repeated install
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install linux-multi --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Test `nix`
|
||||
run: |
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
nix run nixpkgs#fortune
|
||||
- name: Initial uninstall
|
||||
- name: Repeated uninstall
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic uninstall --no-confirm
|
||||
|
||||
run-steam-deck:
|
||||
name: Run Steam Deck (mock)
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-x86_64-linux
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: harmonic-x86_64-linux
|
||||
- name: Set executable
|
||||
run: chmod +x ./harmonic
|
||||
- name: Make the CI look like a steam deck
|
||||
run: |
|
||||
mkdir -p ~/bin
|
||||
echo -e "#! /bin/sh\nexit 0" | sudo tee -a /bin/steamos-readonly
|
||||
sudo chmod +x /bin/steamos-readonly
|
||||
sudo useradd -m deck
|
||||
|
||||
- name: Initial install
|
||||
run: sudo PATH=$PATH:$HOME/bin RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install steam-deck --persistence `pwd`/ci-test-nix --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Initial uninstall (without a `nix run` first)
|
||||
run: sudo PATH=$PATH:$HOME/bin RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic uninstall --no-confirm
|
||||
|
||||
- name: Repeated install
|
||||
run: sudo PATH=$PATH:$HOME/bin RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install steam-deck --persistence `pwd`/ci-test-nix --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Test `nix`
|
||||
run: |
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
nix run nixpkgs#fortune
|
||||
- name: Repeated uninstall
|
||||
run: sudo PATH=$PATH:$HOME/bin RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic uninstall --no-confirm
|
||||
|
||||
build-x86_64-darwin:
|
||||
name: Build x86_64 Darwin
|
||||
runs-on: macos-12
|
||||
|
@ -108,17 +145,15 @@ jobs:
|
|||
name: harmonic-x86_64-darwin
|
||||
- name: Set executable
|
||||
run: chmod +x ./harmonic
|
||||
|
||||
- name: Initial install
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install darwin-multi --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Test run
|
||||
run: |
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
nix run nixpkgs#fortune
|
||||
- name: Initial uninstall
|
||||
- name: Initial uninstall (without a `nix run` first)
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic uninstall --no-confirm
|
||||
|
||||
- name: Repeated install
|
||||
run: sudo RUST_LOG=harmonic=trace RUST_BACKTRACE=full ./harmonic install darwin-multi --extra-conf "access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}" --no-confirm
|
||||
- name: Repeated test run
|
||||
- name: Test `nix`
|
||||
run: |
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
nix run nixpkgs#fortune
|
||||
|
|
|
@ -16,6 +16,7 @@ Planned support:
|
|||
+ Note: User deletion is currently unimplemented, you need to use a user with a secure token and `dscl . -delete /Users/_nixbuild*` where `*` is each user number.
|
||||
* [x] Multi-user aarch64 MacOS
|
||||
+ Note: User deletion is currently unimplemented, you need to use a user with a secure token and `dscl . -delete /Users/_nixbuild*` where `*` is each user number.
|
||||
* [x] Valve Steam Deck
|
||||
* [ ] Single-user x86_64 Linux with systemd init
|
||||
* [ ] Single-user aarch64 Linux with systemd init
|
||||
* [ ] Others...
|
||||
|
|
|
@ -17,7 +17,7 @@ impl PlaceNixConfiguration {
|
|||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(
|
||||
nix_build_group_name: String,
|
||||
extra_conf: Option<String>,
|
||||
extra_conf: Vec<String>,
|
||||
force: bool,
|
||||
) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let buf = format!(
|
||||
|
@ -30,7 +30,7 @@ impl PlaceNixConfiguration {
|
|||
\n\
|
||||
auto-optimise-store = true\n\
|
||||
",
|
||||
extra_conf = extra_conf.unwrap_or_else(|| "".into()),
|
||||
extra_conf = extra_conf.join("\n"),
|
||||
);
|
||||
let create_directory =
|
||||
CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force).await?;
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
|
||||
const SERVICE_SRC: &str = "/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.service";
|
||||
const SOCKET_SRC: &str = "/nix/var/nix/profiles/default/lib/systemd/system/nix-daemon.socket";
|
||||
const TMPFILES_SRC: &str = "/nix/var/nix/profiles/default//lib/tmpfiles.d/nix-daemon.conf";
|
||||
const TMPFILES_SRC: &str = "/nix/var/nix/profiles/default/lib/tmpfiles.d/nix-daemon.conf";
|
||||
const TMPFILES_DEST: &str = "/etc/tmpfiles.d/nix-daemon.conf";
|
||||
const DARWIN_NIX_DAEMON_DEST: &str = "/Library/LaunchDaemons/org.nixos.nix-daemon.plist";
|
||||
|
||||
|
@ -153,8 +153,7 @@ impl Action for ConfigureNixDaemonService {
|
|||
.process_group(0)
|
||||
.arg("enable")
|
||||
.arg("--now")
|
||||
.arg("nix-daemon.socket")
|
||||
.stdin(std::process::Stdio::null()),
|
||||
.arg("nix-daemon.socket"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ConfigureNixDaemonServiceError::Command(e).boxed())?;
|
||||
|
@ -284,6 +283,8 @@ pub enum ConfigureNixDaemonServiceError {
|
|||
std::path::PathBuf,
|
||||
#[source] std::io::Error,
|
||||
),
|
||||
#[error("Set mode `{0}` on `{1}`")]
|
||||
SetPermissions(u32, std::path::PathBuf, #[source] std::io::Error),
|
||||
#[error("Command failed to execute")]
|
||||
Command(#[source] std::io::Error),
|
||||
#[error("Remove file `{0}`")]
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError};
|
||||
use crate::action::StatefulAction;
|
||||
use crate::{
|
||||
action::{Action, ActionDescription},
|
||||
BoxableError,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const PATHS: &[&str] = &[
|
||||
"usr",
|
||||
"usr/lib",
|
||||
"usr/lib/extension-release.d",
|
||||
"usr/lib/systemd",
|
||||
"usr/lib/systemd/system",
|
||||
];
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct CreateSystemdSysext {
|
||||
destination: PathBuf,
|
||||
create_directories: Vec<StatefulAction<CreateDirectory>>,
|
||||
create_extension_release: StatefulAction<CreateFile>,
|
||||
create_bind_mount_unit: StatefulAction<CreateFile>,
|
||||
}
|
||||
|
||||
impl CreateSystemdSysext {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(
|
||||
destination: impl AsRef<Path>,
|
||||
) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let destination = destination.as_ref();
|
||||
|
||||
let mut create_directories =
|
||||
vec![CreateDirectory::plan(destination, None, None, 0o0755, true).await?];
|
||||
for path in PATHS {
|
||||
create_directories.push(
|
||||
CreateDirectory::plan(destination.join(path), None, None, 0o0755, false).await?,
|
||||
)
|
||||
}
|
||||
|
||||
let version_id = tokio::fs::read_to_string("/etc/os-release")
|
||||
.await
|
||||
.map(|buf| {
|
||||
buf.lines()
|
||||
.find_map(|line| match line.starts_with("VERSION_ID") {
|
||||
true => line.rsplit("=").next().map(|inner| inner.to_owned()),
|
||||
false => None,
|
||||
})
|
||||
})
|
||||
.map_err(|e| CreateSystemdSysextError::ReadingOsRelease(e).boxed())?
|
||||
.ok_or_else(|| CreateSystemdSysextError::NoVersionId.boxed())?;
|
||||
let extension_release_buf =
|
||||
format!("SYSEXT_LEVEL=1.0\nID=steamos\nVERSION_ID={version_id}");
|
||||
let create_extension_release = CreateFile::plan(
|
||||
destination.join("usr/lib/extension-release.d/extension-release.nix"),
|
||||
None,
|
||||
None,
|
||||
0o0755,
|
||||
extension_release_buf,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let create_bind_mount_buf = format!(
|
||||
"
|
||||
[Mount]\n\
|
||||
What={}\n\
|
||||
Where=/nix\n\
|
||||
Type=none\n\
|
||||
Options=bind\n\
|
||||
",
|
||||
destination.display(),
|
||||
);
|
||||
let create_bind_mount_unit = CreateFile::plan(
|
||||
destination.join("usr/lib/systemd/system/nix.mount"),
|
||||
None,
|
||||
None,
|
||||
0o0755,
|
||||
create_bind_mount_buf,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
destination: destination.to_path_buf(),
|
||||
create_directories,
|
||||
create_extension_release,
|
||||
create_bind_mount_unit,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "create_systemd_sysext")]
|
||||
impl Action for CreateSystemdSysext {
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
format!(
|
||||
"Create a systemd sysext at `{}`",
|
||||
self.destination.display()
|
||||
)
|
||||
}
|
||||
|
||||
fn execute_description(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(
|
||||
self.tracing_synopsis(),
|
||||
vec![format!(
|
||||
"Create a writable, persistent systemd system extension.",
|
||||
)],
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(destination,))]
|
||||
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let Self {
|
||||
destination: _,
|
||||
create_directories,
|
||||
create_extension_release,
|
||||
create_bind_mount_unit,
|
||||
} = self;
|
||||
|
||||
for create_directory in create_directories {
|
||||
create_directory.try_execute().await?;
|
||||
}
|
||||
create_extension_release.try_execute().await?;
|
||||
create_bind_mount_unit.try_execute().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(
|
||||
format!(
|
||||
"Remove the sysext located at `{}`",
|
||||
self.destination.display()
|
||||
),
|
||||
vec![],
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(destination,))]
|
||||
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let Self {
|
||||
destination: _,
|
||||
create_directories,
|
||||
create_extension_release,
|
||||
create_bind_mount_unit,
|
||||
} = self;
|
||||
|
||||
create_bind_mount_unit.try_revert().await?;
|
||||
|
||||
create_extension_release.try_revert().await?;
|
||||
|
||||
for create_directory in create_directories.iter_mut().rev() {
|
||||
create_directory.try_revert().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CreateSystemdSysextError {
|
||||
#[error(transparent)]
|
||||
CreateDirectory(#[from] CreateDirectoryError),
|
||||
#[error(transparent)]
|
||||
CreateFile(#[from] CreateFileError),
|
||||
#[error("Reading /etc/os-release")]
|
||||
ReadingOsRelease(
|
||||
#[source]
|
||||
#[from]
|
||||
std::io::Error,
|
||||
),
|
||||
#[error("No `VERSION_ID` line in /etc/os-release")]
|
||||
NoVersionId,
|
||||
}
|
|
@ -1,9 +1,5 @@
|
|||
//! [`Action`](crate::action::Action)s for Linux based systems
|
||||
|
||||
mod configure_nix_daemon_service;
|
||||
mod create_systemd_sysext;
|
||||
mod start_systemd_unit;
|
||||
|
||||
pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError};
|
||||
pub use create_systemd_sysext::{CreateSystemdSysext, CreateSystemdSysextError};
|
||||
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use tokio::process::Command;
|
||||
|
||||
use crate::action::StatefulAction;
|
||||
use crate::action::{ActionState, StatefulAction};
|
||||
use crate::execute_command;
|
||||
|
||||
use crate::{
|
||||
|
@ -19,9 +19,14 @@ pub struct StartSystemdUnit {
|
|||
impl StartSystemdUnit {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(
|
||||
unit: String,
|
||||
unit: impl AsRef<str>,
|
||||
) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(Self { unit }.into())
|
||||
Ok(StatefulAction {
|
||||
action: Self {
|
||||
unit: unit.as_ref().to_string(),
|
||||
},
|
||||
state: ActionState::Uncompleted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
use crate::execute_command;
|
||||
use crate::{
|
||||
action::{Action, ActionDescription, ActionState},
|
||||
BoxableError,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct SystemdSysextMerge {
|
||||
device: PathBuf,
|
||||
}
|
||||
|
||||
impl SystemdSysextMerge {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn plan(device: PathBuf) -> Result<Self, SystemdSysextMergeError> {
|
||||
Ok(Self { device })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "systemd_sysext_merge")]
|
||||
impl Action for SystemdSysextMerge {
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
format!("Run `systemd-sysext merge `{}`", device.display())
|
||||
}
|
||||
|
||||
fn describe_execute(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(
|
||||
device = %self.device.display(),
|
||||
))]
|
||||
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let Self {
|
||||
device,
|
||||
action_state,
|
||||
} = self;
|
||||
|
||||
execute_command(
|
||||
Command::new("systemd-sysext")
|
||||
.process_group(0)
|
||||
.arg("merge")
|
||||
.arg(device)
|
||||
.stdin(std::process::Stdio::null()),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SystemdSysextMergeError::Command(e).boxed())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn describe_revert(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(
|
||||
"Stop the systemd Nix service and socket".to_string(),
|
||||
vec![
|
||||
"The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string()
|
||||
]
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(
|
||||
device = %self.device.display(),
|
||||
))]
|
||||
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let Self { device } = self;
|
||||
|
||||
// TODO(@Hoverbear): Handle proxy vars
|
||||
execute_command(
|
||||
Command::new("systemd-sysext")
|
||||
.process_group(0)
|
||||
.arg("unmerge")
|
||||
.arg(device)
|
||||
.stdin(std::process::Stdio::null()),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SystemdSysextMergeError::Command(e).boxed())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SystemdSysextMergeError {
|
||||
#[error("Failed to execute command")]
|
||||
Command(#[source] std::io::Error),
|
||||
}
|
|
@ -49,7 +49,7 @@ use std::{error::Error, collections::HashMap};
|
|||
use harmonic::{
|
||||
InstallPlan,
|
||||
settings::{CommonSettings, InstallSettingsError},
|
||||
planner::{Planner, PlannerError, specific::SteamDeck},
|
||||
planner::{Planner, PlannerError, linux::SteamDeck},
|
||||
action::{Action, StatefulAction, ActionDescription},
|
||||
};
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ Sometimes choosing a specific plan is desired:
|
|||
|
||||
```rust,no_run
|
||||
use std::error::Error;
|
||||
use harmonic::{InstallPlan, planner::{Planner, specific::SteamDeck}};
|
||||
use harmonic::{InstallPlan, planner::{Planner, linux::SteamDeck}};
|
||||
|
||||
# async fn chosen_planner_install() -> color_eyre::Result<()> {
|
||||
let planner = SteamDeck::default().await?;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! Planners for Linux based systems
|
||||
|
||||
mod multi;
|
||||
mod steam_deck;
|
||||
|
||||
pub use multi::LinuxMulti;
|
||||
pub use steam_deck::SteamDeck;
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
|||
action::{
|
||||
base::CreateDirectory,
|
||||
common::{ConfigureNix, ProvisionNix},
|
||||
linux::StartSystemdUnit,
|
||||
StatefulAction,
|
||||
},
|
||||
planner::{Planner, PlannerError},
|
||||
|
@ -58,6 +59,10 @@ impl Planner for LinuxMulti {
|
|||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
StartSystemdUnit::plan("nix-daemon.socket".to_string())
|
||||
.await
|
||||
.map_err(|v| PlannerError::Action(v.into()))?
|
||||
.boxed(),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
246
src/planner/linux/steam_deck.rs
Normal file
246
src/planner/linux/steam_deck.rs
Normal file
|
@ -0,0 +1,246 @@
|
|||
/** Testing the Steam Deck Install (Summary of https://blogs.igalia.com/berto/2022/07/05/running-the-steam-decks-os-in-a-virtual-machine-using-qemu/)
|
||||
|
||||
One time step:
|
||||
|
||||
1. Grab the SteamOS: Steam Deck Image from https://store.steampowered.com/steamos/download/?ver=steamdeck&snr=
|
||||
2. Extract it (this can take a bit)
|
||||
```sh
|
||||
bunzip2 steamdeck-recovery-4.img.bz2
|
||||
```
|
||||
2. Create a disk image
|
||||
```sh
|
||||
qemu-img create -f qcow2 steamos.qcow2 64G
|
||||
```
|
||||
3. Start a VM to run the install onto the created disk
|
||||
|
||||
*Note:*
|
||||
```sh
|
||||
RECOVERY_IMAGE=steamdeck-recovery-4.img
|
||||
nix build "nixpkgs#legacyPackages.x86_64-linux.OVMF.fd" --out-link ovmf
|
||||
qemu-system-x86_64 -enable-kvm -smp cores=4 -m 8G \
|
||||
-device usb-ehci -device usb-tablet \
|
||||
-device intel-hda -device hda-duplex \
|
||||
-device VGA,xres=1280,yres=800 \
|
||||
-drive if=pflash,format=raw,readonly=on,file=ovmf-fd/FV/OVMF.fd \
|
||||
-drive if=virtio,file=$RECOVERY_IMAGE,driver=raw \
|
||||
-device nvme,drive=drive0,serial=badbeef \
|
||||
-drive if=none,id=drive0,file=steamos.qcow2
|
||||
```
|
||||
4. Pick "Reimage Steam Deck". **Important:** when it is done do not reboot the steam deck, hit "Cancel"
|
||||
5. Run `sudo steamos-chroot --disk /dev/nvme0n1 --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
|
||||
```
|
||||
6. Run `sudo steamos-chroot --disk /dev/nvme0n1 --partset B` and inside run the same above commands
|
||||
7. Safely turn off the VM!
|
||||
|
||||
Repeated step:
|
||||
1. Create a snapshot of the base install to work on
|
||||
```sh
|
||||
cp steamos.qcow2 steamos-hack.qcow2
|
||||
2. Run the VM
|
||||
```sh
|
||||
nix build "nixpkgs#legacyPackages.x86_64-linux.OVMF.fd" --out-link ovmf
|
||||
qemu-system-x86_64 -enable-kvm -smp cores=4 -m 8G \
|
||||
-device usb-ehci -device usb-tablet \
|
||||
-device intel-hda -device hda-duplex \
|
||||
-device VGA,xres=1280,yres=800 \
|
||||
-drive if=pflash,format=raw,readonly=on,file=ovmf-fd/FV/OVMF_CODE.fd \
|
||||
-drive if=pflash,format=raw,readonly=on,file=ovmf-fd/FV/OVMF_VARS.fd \
|
||||
-drive if=virtio,file=steamos-hack.qcow2 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22
|
||||
```
|
||||
3. **Do your testing!** You can `ssh deck@localhost -p 2222` in and use `rsync -e 'ssh -p 2222' result/bin/harmonic deck@localhost:harmonic` to send a harmonic build.
|
||||
4. Delete `steamos-hack.qcow2`
|
||||
*/
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
action::{
|
||||
base::{CreateDirectory, CreateFile},
|
||||
common::{ConfigureNix, ProvisionNix},
|
||||
linux::StartSystemdUnit,
|
||||
Action, StatefulAction,
|
||||
},
|
||||
planner::{Planner, PlannerError},
|
||||
settings::{CommonSettings, InstallSettingsError},
|
||||
BuiltinPlanner,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SteamDeck {
|
||||
#[clap(
|
||||
long,
|
||||
env = "HARMONIC_STEAM_DECK_PERSISTENCE",
|
||||
default_value = "/home/nix"
|
||||
)]
|
||||
persistence: PathBuf,
|
||||
#[clap(flatten)]
|
||||
pub settings: CommonSettings,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "steam-deck")]
|
||||
impl Planner for SteamDeck {
|
||||
async fn default() -> Result<Self, PlannerError> {
|
||||
Ok(Self {
|
||||
persistence: PathBuf::from("/home/nix"),
|
||||
settings: CommonSettings::default()?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
|
||||
let persistence = &self.persistence;
|
||||
|
||||
let nix_directory_buf = format!(
|
||||
"
|
||||
[Unit]\n\
|
||||
Description=Create a `/nix` directory to be used for bind mounting\n\
|
||||
PropagatesStopTo=nix-daemon.service\n\
|
||||
PropagatesStopTo=nix.mount\n\
|
||||
DefaultDependencies=no\n\
|
||||
\n\
|
||||
[Service]\n\
|
||||
Type=oneshot\n\
|
||||
ExecCondition=sh -c \"if [ -d /nix ]; then exit 1; else exit 0; fi\"
|
||||
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!(
|
||||
"\
|
||||
[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\
|
||||
",
|
||||
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)?;
|
||||
|
||||
let ensure_symlinked_units_resolve_buf = format!(
|
||||
"\
|
||||
[Unit]\n\
|
||||
Description=Ensure Nix related units which are symlinked resolve\n\
|
||||
After=nix.mount\n\
|
||||
Requires=nix-directory.service\n\
|
||||
Requires=nix.mount\n\
|
||||
PropagatesStopTo=nix-directory.service\n\
|
||||
PropagatesStopTo=nix.mount\n\
|
||||
DefaultDependencies=no\n\
|
||||
\n\
|
||||
[Service]\n\
|
||||
Type=oneshot\n\
|
||||
RemainAfterExit=yes\n\
|
||||
ExecStart=/usr/bin/systemctl daemon-reload\n\
|
||||
ExecStart=/usr/bin/systemctl restart --no-block sockets.target timers.target multi-user.target\n\
|
||||
\n\
|
||||
[Install]\n\
|
||||
WantedBy=sysinit.target\n\
|
||||
"
|
||||
);
|
||||
let ensure_symlinked_units_resolve_unit = CreateFile::plan(
|
||||
"/etc/systemd/system/ensure-symlinked-units-resolve.service",
|
||||
None,
|
||||
None,
|
||||
0o0644,
|
||||
ensure_symlinked_units_resolve_buf,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(PlannerError::Action)?;
|
||||
|
||||
Ok(vec![
|
||||
CreateDirectory::plan(&persistence, None, None, 0o0755, true)
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
nix_directory_unit.boxed(),
|
||||
create_bind_mount_unit.boxed(),
|
||||
ensure_symlinked_units_resolve_unit.boxed(),
|
||||
StartSystemdUnit::plan("ensure-symlinked-units-resolve.service".to_string())
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
ProvisionNix::plan(&self.settings.clone())
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
ConfigureNix::plan(&self.settings)
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
StartSystemdUnit::plan("nix-daemon.socket".to_string())
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
])
|
||||
}
|
||||
|
||||
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
|
||||
let Self {
|
||||
settings,
|
||||
persistence,
|
||||
} = self;
|
||||
let mut map = HashMap::default();
|
||||
|
||||
map.extend(settings.settings()?.into_iter());
|
||||
map.insert(
|
||||
"persistence".to_string(),
|
||||
serde_json::to_value(persistence)?,
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<BuiltinPlanner> for SteamDeck {
|
||||
fn into(self) -> BuiltinPlanner {
|
||||
BuiltinPlanner::SteamDeck(self)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
/*! [`BuiltinPlanner`]s and traits to create new types which can be used to plan out an [`InstallPlan`]
|
||||
|
||||
It's a [`Planner`]s job to construct (if possible) a valid [`InstallPlan`] for the host. Some planners,
|
||||
like [`LinuxMulti`](linux::LinuxMulti), are operating system specific. Others, like [`SteamDeck`](specific::SteamDeck), are device specific.
|
||||
like [`LinuxMulti`](linux::LinuxMulti), are operating system specific. Others, like [`SteamDeck`](linux::SteamDeck), are device specific.
|
||||
|
||||
[`Planner`]s contain their planner specific settings, typically alongside a [`CommonSettings`][crate::settings::CommonSettings].
|
||||
|
||||
|
@ -16,7 +16,7 @@ use std::{error::Error, collections::HashMap};
|
|||
use harmonic::{
|
||||
InstallPlan,
|
||||
settings::{CommonSettings, InstallSettingsError},
|
||||
planner::{Planner, PlannerError, specific::SteamDeck},
|
||||
planner::{Planner, PlannerError, linux::SteamDeck},
|
||||
action::{Action, StatefulAction, linux::StartSystemdUnit},
|
||||
};
|
||||
|
||||
|
@ -39,7 +39,7 @@ impl Planner for MyPlanner {
|
|||
Ok(vec![
|
||||
// ...
|
||||
|
||||
StartSystemdUnit::plan("nix-daemon.socket".into())
|
||||
StartSystemdUnit::plan("nix-daemon.socket")
|
||||
.await
|
||||
.map_err(PlannerError::Action)?.boxed(),
|
||||
])
|
||||
|
@ -76,7 +76,6 @@ match plan.install(None).await {
|
|||
*/
|
||||
pub mod darwin;
|
||||
pub mod linux;
|
||||
pub mod specific;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -115,8 +114,8 @@ pub enum BuiltinPlanner {
|
|||
LinuxMulti(linux::LinuxMulti),
|
||||
/// A standard MacOS (Darwin) multi-user install
|
||||
DarwinMulti(darwin::DarwinMulti),
|
||||
/// An install suitable for the Valve Steam Deck console
|
||||
SteamDeck(specific::SteamDeck),
|
||||
/// A specialized install suitable for the Valve Steam Deck console
|
||||
SteamDeck(linux::SteamDeck),
|
||||
}
|
||||
|
||||
impl BuiltinPlanner {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
mod steam_deck;
|
||||
|
||||
pub use steam_deck::SteamDeck;
|
|
@ -1,68 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
action::{
|
||||
base::CreateDirectory,
|
||||
common::ProvisionNix,
|
||||
linux::{CreateSystemdSysext, StartSystemdUnit},
|
||||
StatefulAction,
|
||||
},
|
||||
planner::{Planner, PlannerError},
|
||||
settings::CommonSettings,
|
||||
settings::InstallSettingsError,
|
||||
Action, BuiltinPlanner,
|
||||
};
|
||||
|
||||
/// A planner suitable for Valve Steam Deck consoles
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "cli", derive(clap::Parser))]
|
||||
pub struct SteamDeck {
|
||||
#[cfg_attr(feature = "cli", clap(flatten))]
|
||||
pub settings: CommonSettings,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "steam-deck")]
|
||||
impl Planner for SteamDeck {
|
||||
async fn default() -> Result<Self, PlannerError> {
|
||||
Ok(Self {
|
||||
settings: CommonSettings::default()?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
|
||||
Ok(vec![
|
||||
CreateSystemdSysext::plan("/var/lib/extensions/nix")
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
CreateDirectory::plan("/nix", None, None, 0o0755, true)
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
ProvisionNix::plan(&self.settings.clone())
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
StartSystemdUnit::plan("nix-daemon.socket".into())
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
])
|
||||
}
|
||||
|
||||
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
|
||||
let Self { settings } = self;
|
||||
let mut map = HashMap::default();
|
||||
|
||||
map.extend(settings.settings()?.into_iter());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<BuiltinPlanner> for SteamDeck {
|
||||
fn into(self) -> BuiltinPlanner {
|
||||
BuiltinPlanner::SteamDeck(self)
|
||||
}
|
||||
}
|
|
@ -127,8 +127,8 @@ pub struct CommonSettings {
|
|||
pub(crate) nix_package_url: Url,
|
||||
|
||||
/// Extra configuration lines for `/etc/nix.conf`
|
||||
#[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_EXTRA_CONF"))]
|
||||
pub(crate) extra_conf: Option<String>,
|
||||
#[clap(long, env = "HARMONIC_EXTRA_CONF")]
|
||||
pub extra_conf: Vec<String>,
|
||||
|
||||
/// If Harmonic should forcibly recreate files it finds existing
|
||||
#[cfg_attr(
|
||||
|
@ -309,9 +309,8 @@ impl CommonSettings {
|
|||
self.nix_package_url = url;
|
||||
self
|
||||
}
|
||||
|
||||
/// Extra configuration lines for `/etc/nix.conf`
|
||||
pub fn extra_conf(&mut self, extra_conf: Option<String>) -> &mut Self {
|
||||
pub fn extra_conf(&mut self, extra_conf: Vec<String>) -> &mut Self {
|
||||
self.extra_conf = extra_conf;
|
||||
self
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue