diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c951a04..ded4c35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index fc37909..cda95c5 100644 --- a/README.md +++ b/README.md @@ -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... diff --git a/src/action/common/place_nix_configuration.rs b/src/action/common/place_nix_configuration.rs index 6116f4c..3985560 100644 --- a/src/action/common/place_nix_configuration.rs +++ b/src/action/common/place_nix_configuration.rs @@ -17,7 +17,7 @@ impl PlaceNixConfiguration { #[tracing::instrument(skip_all)] pub async fn plan( nix_build_group_name: String, - extra_conf: Option, + extra_conf: Vec, force: bool, ) -> Result, Box> { 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?; diff --git a/src/action/linux/configure_nix_daemon_service.rs b/src/action/linux/configure_nix_daemon_service.rs index 362cb02..9f3a979 100644 --- a/src/action/linux/configure_nix_daemon_service.rs +++ b/src/action/linux/configure_nix_daemon_service.rs @@ -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}`")] diff --git a/src/action/linux/create_systemd_sysext.rs b/src/action/linux/create_systemd_sysext.rs deleted file mode 100644 index 740ec14..0000000 --- a/src/action/linux/create_systemd_sysext.rs +++ /dev/null @@ -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>, - create_extension_release: StatefulAction, - create_bind_mount_unit: StatefulAction, -} - -impl CreateSystemdSysext { - #[tracing::instrument(skip_all)] - pub async fn plan( - destination: impl AsRef, - ) -> Result, Box> { - 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 { - 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> { - 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 { - 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> { - 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, -} diff --git a/src/action/linux/mod.rs b/src/action/linux/mod.rs index dfab38f..55a3767 100644 --- a/src/action/linux/mod.rs +++ b/src/action/linux/mod.rs @@ -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}; diff --git a/src/action/linux/start_systemd_unit.rs b/src/action/linux/start_systemd_unit.rs index 6687d77..23412d8 100644 --- a/src/action/linux/start_systemd_unit.rs +++ b/src/action/linux/start_systemd_unit.rs @@ -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, ) -> Result, Box> { - Ok(Self { unit }.into()) + Ok(StatefulAction { + action: Self { + unit: unit.as_ref().to_string(), + }, + state: ActionState::Uncompleted, + }) } } diff --git a/src/action/linux/systemd_sysext_merge.rs b/src/action/linux/systemd_sysext_merge.rs deleted file mode 100644 index 9511725..0000000 --- a/src/action/linux/systemd_sysext_merge.rs +++ /dev/null @@ -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 { - 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 { - vec![ActionDescription::new(self.tracing_synopsis(), vec![])] - } - - #[tracing::instrument(skip_all, fields( - device = %self.device.display(), - ))] - async fn execute(&mut self) -> Result<(), Box> { - 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 { - 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> { - 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), -} diff --git a/src/action/mod.rs b/src/action/mod.rs index a4afa06..d7d723f 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -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}, }; diff --git a/src/lib.rs b/src/lib.rs index bb27088..2d47cf6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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?; diff --git a/src/planner/linux/mod.rs b/src/planner/linux/mod.rs index 6f81f90..e050ec8 100644 --- a/src/planner/linux/mod.rs +++ b/src/planner/linux/mod.rs @@ -1,5 +1,7 @@ //! Planners for Linux based systems mod multi; +mod steam_deck; pub use multi::LinuxMulti; +pub use steam_deck::SteamDeck; diff --git a/src/planner/linux/multi.rs b/src/planner/linux/multi.rs index a39dca9..911b6fe 100644 --- a/src/planner/linux/multi.rs +++ b/src/planner/linux/multi.rs @@ -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(), ]) } diff --git a/src/planner/linux/steam_deck.rs b/src/planner/linux/steam_deck.rs new file mode 100644 index 0000000..5f297e9 --- /dev/null +++ b/src/planner/linux/steam_deck.rs @@ -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 { + Ok(Self { + persistence: PathBuf::from("/home/nix"), + settings: CommonSettings::default()?, + }) + } + + async fn plan(&self) -> Result>>, 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, 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 for SteamDeck { + fn into(self) -> BuiltinPlanner { + BuiltinPlanner::SteamDeck(self) + } +} diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 628084b..cc441bb 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -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 { diff --git a/src/planner/specific/mod.rs b/src/planner/specific/mod.rs deleted file mode 100644 index 1ef7ac4..0000000 --- a/src/planner/specific/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod steam_deck; - -pub use steam_deck::SteamDeck; diff --git a/src/planner/specific/steam_deck.rs b/src/planner/specific/steam_deck.rs deleted file mode 100644 index ab64370..0000000 --- a/src/planner/specific/steam_deck.rs +++ /dev/null @@ -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 { - Ok(Self { - settings: CommonSettings::default()?, - }) - } - - async fn plan(&self) -> Result>>, 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, InstallSettingsError> { - let Self { settings } = self; - let mut map = HashMap::default(); - - map.extend(settings.settings()?.into_iter()); - - Ok(map) - } -} - -impl Into for SteamDeck { - fn into(self) -> BuiltinPlanner { - BuiltinPlanner::SteamDeck(self) - } -} diff --git a/src/settings.rs b/src/settings.rs index dcc4f11..9dd2b9a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -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, + #[clap(long, env = "HARMONIC_EXTRA_CONF")] + pub extra_conf: Vec, /// 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) -> &mut Self { + pub fn extra_conf(&mut self, extra_conf: Vec) -> &mut Self { self.extra_conf = extra_conf; self }