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:
Ana Hobden 2022-12-02 07:31:15 -08:00 committed by GitHub
parent 0e2f27713f
commit ae37842a0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 321 additions and 366 deletions

View file

@ -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

View file

@ -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...

View file

@ -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?;

View file

@ -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}`")]

View file

@ -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,
}

View file

@ -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};

View file

@ -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,
})
}
}

View file

@ -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),
}

View file

@ -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},
};

View file

@ -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?;

View file

@ -1,5 +1,7 @@
//! Planners for Linux based systems
mod multi;
mod steam_deck;
pub use multi::LinuxMulti;
pub use steam_deck::SteamDeck;

View file

@ -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(),
])
}

View 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)
}
}

View file

@ -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 {

View file

@ -1,3 +0,0 @@
mod steam_deck;
pub use steam_deck::SteamDeck;

View file

@ -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)
}
}

View file

@ -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
}