├── .gitignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── lichen_logo.png ├── workflows │ ├── typos.yml │ └── ci.yaml └── dependabot.yml ├── rustfmt.toml ├── .typos.toml ├── crates ├── system │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── disk │ │ │ ├── mod.rs │ │ │ ├── partition.rs │ │ │ └── disks.rs │ │ └── locale │ │ │ ├── mod.rs │ │ │ ├── iso_639_2.rs │ │ │ ├── iso_3166.rs │ │ │ ├── iso_639_3.rs │ │ │ └── registry.rs │ └── Cargo.toml └── installer │ ├── README.md │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── systemd.rs │ ├── model.rs │ ├── steps │ ├── context.rs │ ├── cleanup.rs │ ├── packaging.rs │ ├── partitions.rs │ ├── mod.rs │ └── postinstall.rs │ ├── partitions.rs │ ├── account.rs │ ├── selections.rs │ └── engine.rs ├── selections ├── kernel-common.json ├── plasma-plm.json ├── develop.json ├── plasma-sddm.json ├── plasma-shared.json ├── server.json ├── sway.json ├── base-desktop.json ├── README.md ├── kernel-desktop.json ├── gnome.json ├── cosmic.json └── base.json ├── lichen_ipc ├── build.rs ├── src │ ├── lib.rs │ ├── com.serpentos.lichen.disks.varlink │ ├── main.rs │ ├── disks.rs │ └── com_serpentos_lichen_disks.rs ├── Cargo.toml └── README.md ├── lichen_cli ├── Cargo.toml ├── build.rs └── src │ └── main.rs ├── README.md ├── Cargo.toml ├── LICENSE ├── LICENSES └── MPL-2.0.txt └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * AerynOS/tooling:core 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 120 3 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | hax = "hax" # slang: hacks 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ikeycode] 4 | -------------------------------------------------------------------------------- /.github/lichen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AerynOS/lichen/HEAD/.github/lichen_logo.png -------------------------------------------------------------------------------- /crates/system/README.md: -------------------------------------------------------------------------------- 1 | # system 2 | 3 | This crate provides various probes and data APIs to learn more about the 4 | underlying system, such as locales, disk data, etc. 5 | -------------------------------------------------------------------------------- /crates/installer/README.md: -------------------------------------------------------------------------------- 1 | # installer 2 | 3 | This crate provides the core model of the installer, absorbing the various features of `system` detection and a way to execute the steps. 4 | -------------------------------------------------------------------------------- /crates/system/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Integration with various system APIs to enumerate locales, timezones, etc. 6 | 7 | pub mod disk; 8 | pub mod locale; 9 | -------------------------------------------------------------------------------- /selections/kernel-common.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kernel-common", 3 | "summary": "Common kernel runtime", 4 | "description": "Common kernel runtime", 5 | "required": [ 6 | "intel-microcode", 7 | "kmod", 8 | "linux-firmware-amd64-microcode", 9 | "lvm2" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: Typos 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | typos: 11 | name: Check for typos 12 | runs-on: ubuntu-24.04 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: crate-ci/typos@v1.34.0 17 | -------------------------------------------------------------------------------- /lichen_ipc/build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | fn main() { 6 | println!("cargo:rerun-if-changed=src/com.serpentos.lichen.disks.varlink"); 7 | varlink_generator::cargo_build_tosource("src/com.serpentos.lichen.disks.varlink", true); 8 | } 9 | -------------------------------------------------------------------------------- /crates/system/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "system" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | gpt = "4.0.0" 8 | thiserror.workspace = true 9 | serde_json.workspace = true 10 | serde.workspace = true 11 | superblock.workspace = true 12 | fs-err.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /selections/plasma-plm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plasma-plm", 3 | "summary": "Plasma Desktop - plasma-login-manager (EXPERIMENTAL)", 4 | "description": "EXPERIMENTAL", 5 | "display": false, 6 | "priority": 90, 7 | "depends": [ 8 | "plasma-shared" 9 | ], 10 | "required": [ 11 | "binary(plasmalogin)" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /selections/develop.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "develop", 3 | "summary": "Development tooling", 4 | "description": "Install software for packaging and development.", 5 | "depends": [ 6 | "base" 7 | ], 8 | "required": [ 9 | "binary(boulder)", 10 | "binary(git)", 11 | "binary(yq)", 12 | "binary(zed)" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /selections/plasma-sddm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plasma-sddm", 3 | "summary": "Plasma Desktop - SDDM Login Manager", 4 | "description": "Recommended for most users", 5 | "display": true, 6 | "priority": 15, 7 | "depends": [ 8 | "plasma-shared" 9 | ], 10 | "required": [ 11 | "binary(sddm)", 12 | "sddm-kcm" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /selections/plasma-shared.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plasma-shared", 3 | "summary": "Plasma Desktop - Shared components", 4 | "description": "Recommended for most users", 5 | "depends": [ 6 | "base-desktop" 7 | ], 8 | "required": [ 9 | "pkgset-aeryn-plasma-minimal", 10 | "pkgset-aeryn-plasma-recommended", 11 | "branding-aeryn-plasma" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /selections/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "summary": "Console-only - no WM/DE installed (EXPERTS ONLY)", 4 | "description": "Are you sure? Like, REALLY sure?", 5 | "display": true, 6 | "priority": 100, 7 | "depends": [ 8 | "base" 9 | ], 10 | "required": [ 11 | "networkmanager", 12 | "binary(nmtui)", 13 | "sysbinary(sshd)" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /selections/sway.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sway", 3 | "summary": "Sway - A tiling Wayland compositor inspired by i3", 4 | "description": "Flexible DIY Desktop Experience", 5 | "display": false, 6 | "priority": 70, 7 | "depends": [ 8 | "base-desktop" 9 | ], 10 | "required": [ 11 | "binary(alacritty)", 12 | "binary(sddm)", 13 | "pkgset-aeryn-sway-minimal", 14 | "branding-aeryn-sway" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "cargo" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | day: "tuesday" 10 | time: "10:00" 11 | timezone: "Europe/London" 12 | groups: 13 | cargo: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /lichen_ipc/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | #[allow( 6 | dead_code, 7 | elided_lifetimes_in_paths, 8 | unused_imports, 9 | unused_qualifications, 10 | clippy::needless_lifetimes 11 | )] 12 | mod com_serpentos_lichen_disks; 13 | pub mod disks_ipc { 14 | pub use crate::com_serpentos_lichen_disks::*; 15 | } 16 | 17 | pub mod disks; 18 | -------------------------------------------------------------------------------- /crates/installer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "installer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | itertools.workspace = true 8 | libc.workspace = true 9 | log.workspace = true 10 | human_bytes.workspace = true 11 | serde.workspace = true 12 | serde_json.workspace = true 13 | system = { path = "../system" } 14 | thiserror.workspace = true 15 | topology.workspace = true 16 | fs-err.workspace = true 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /selections/base-desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base-desktop", 3 | "summary": "Common desktop components", 4 | "description": "Common desktop components", 5 | "depends": [ 6 | "base" 7 | ], 8 | "required": [ 9 | "binary(firefox)", 10 | "flatpak", 11 | "font-hack", 12 | "font-noto", 13 | "font-noto-emoji", 14 | "liberation-fonts-ttf", 15 | "scx-scheds", 16 | "sysbinary(plymouthd)", 17 | "pkgset-aeryn-base-desktop", 18 | "zram-generator-defaults" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /crates/installer/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Lichen installer APIs 6 | 7 | mod model; 8 | 9 | pub use model::Model; 10 | 11 | mod account; 12 | pub use account::Account; 13 | 14 | mod engine; 15 | pub use engine::Installer; 16 | 17 | mod partitions; 18 | pub use partitions::{BootPartition, SystemPartition}; 19 | 20 | pub mod systemd; 21 | 22 | pub use system::locale::Locale; 23 | 24 | pub mod steps; 25 | 26 | pub mod selections; 27 | -------------------------------------------------------------------------------- /selections/README.md: -------------------------------------------------------------------------------- 1 | # selections 2 | 3 | This directory contains some selections for the target installation experience. 4 | 5 | ### base 6 | 7 | The mandatory bottom layer for all systems 8 | 9 | ### cosmic 10 | 11 | A full COSMIC desktop 12 | 13 | ### develop 14 | 15 | A set of additional packages to make AerynOS development easier. 16 | 17 | ### gnome 18 | 19 | A full GNOME desktop featuring GDM 20 | 21 | ### kernel-common 22 | 23 | Required common pieces for kernels 24 | 25 | ### kernel-desktop 26 | 27 | Desktop specific kernel, firmware, etc. 28 | -------------------------------------------------------------------------------- /lichen_ipc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lichen_ipc" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [build-dependencies] 7 | varlink_generator.workspace = true 8 | 9 | [dependencies] 10 | log.workspace = true 11 | system = { path = "../crates/system"} 12 | nix.workspace = true 13 | serde.workspace = true 14 | serde_derive.workspace = true 15 | serde_json.workspace = true 16 | varlink.workspace = true 17 | clap = { version = "4.5.21", features = ["derive"] } 18 | color-eyre.workspace = true 19 | pretty_env_logger = { version = "0.5.0" } 20 | 21 | [lints] 22 | workspace = true 23 | -------------------------------------------------------------------------------- /lichen_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lichen_cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | color-eyre.workspace = true 9 | chrono-tz.workspace = true 10 | crossterm.workspace = true 11 | cliclack = { git = "https://github.com/ikeycode/cliclack.git", rev = "35a1882c601b90bf1398c3cb867cc6b20bbe9ce9" } 12 | dialoguer = { version = "0.11.0", features = ["completion", "fuzzy-matcher", "fuzzy-select"] } 13 | indicatif = "0.17.8" 14 | indoc = "2.0.5" 15 | installer = { path = "../crates/installer" } 16 | console = "0.15.8" 17 | nix.workspace = true 18 | env_logger.workspace = true 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /lichen_ipc/src/com.serpentos.lichen.disks.varlink: -------------------------------------------------------------------------------- 1 | # Disk enumeration APIs for Lichen 2 | interface com.serpentos.lichen.disks 3 | 4 | type Disk( 5 | kind: (ssd, hdd), 6 | path: string, 7 | model: ?string, 8 | vendor: ?string, 9 | size: int, 10 | block_size: int 11 | ) 12 | 13 | type Partition( 14 | path: string, 15 | kind: (esp, xbootldr, regular), 16 | size: int, 17 | uuid: string, 18 | superblock_kind: (btrfs, ext4, f2fs, luks2, xfs, unknown) 19 | ) 20 | 21 | error DiskError( 22 | message: string 23 | ) 24 | 25 | # Enumerate all known disks 26 | method GetDisks() -> (disks:[]Disk) 27 | method GetPartitions(disk: string) -> (partitions:[]Partition) 28 | -------------------------------------------------------------------------------- /crates/installer/src/systemd.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! systemd helpers 6 | 7 | use std::{io, process::Command, string::FromUtf8Error}; 8 | 9 | use thiserror::Error; 10 | 11 | #[derive(Debug, Error)] 12 | pub enum Error { 13 | #[error("io: {0}")] 14 | IO(#[from] io::Error), 15 | 16 | #[error("utf8 decoding: {0}")] 17 | Utf8(#[from] FromUtf8Error), 18 | } 19 | 20 | /// List all locales according to localectl 21 | pub fn localectl_list_locales() -> Result, Error> { 22 | let output = Command::new("localectl").arg("list-locales").output()?; 23 | let text = String::from_utf8(output.stdout)?; 24 | Ok(text.lines().map(|l| l.to_string()).collect::>()) 25 | } 26 | -------------------------------------------------------------------------------- /selections/kernel-desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kernel-desktop", 3 | "summary": "Desktop kernel", 4 | "description": "Kernel optimized for desktop use", 5 | "depends": [ 6 | "kernel-common" 7 | ], 8 | "required": [ 9 | "linux-desktop", 10 | "linux-firmware", 11 | "linux-firmware-amd-graphics", 12 | "linux-firmware-atheros", 13 | "linux-firmware-brcm80211", 14 | "linux-firmware-cirrus", 15 | "linux-firmware-intel-graphics", 16 | "linux-firmware-intel-misc", 17 | "linux-firmware-intel-sound", 18 | "linux-firmware-iwlwifi", 19 | "linux-firmware-libertas", 20 | "linux-firmware-mediatek", 21 | "linux-firmware-misc", 22 | "linux-firmware-nvidia-graphics", 23 | "linux-firmware-realtek", 24 | "linux-firmware-ti-connectivity", 25 | "sof-firmware" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /crates/installer/src/model.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | use std::collections::BTreeSet; 6 | 7 | use system::locale::Locale; 8 | 9 | use crate::{Account, BootPartition, SystemPartition}; 10 | 11 | /// Core model for the installation target 12 | #[derive(Debug)] 13 | pub struct Model<'a> { 14 | /// All accounts in the system. 15 | pub accounts: BTreeSet, 16 | 17 | /// The boot partition to use 18 | pub boot_partition: BootPartition, 19 | 20 | /// The system partitions to use/mount 21 | pub partitions: Vec, 22 | 23 | /// System locale to set 24 | pub locale: Option<&'a Locale<'a>>, 25 | 26 | /// Timezone ID 27 | pub timezone: Option, 28 | 29 | /// Package selections 30 | pub packages: BTreeSet, 31 | 32 | /// rootfs format 33 | pub rootfs_type: String, 34 | } 35 | -------------------------------------------------------------------------------- /crates/system/src/disk/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Disk management 6 | 7 | use std::{io, num::ParseIntError}; 8 | 9 | use thiserror::Error; 10 | 11 | /// Error reporting for disks 12 | #[derive(Debug, Error)] 13 | pub enum Error { 14 | #[error("io: {0}")] 15 | IO(#[from] io::Error), 16 | 17 | #[error("gpt: {0}")] 18 | Gpt(#[from] gpt::GptError), 19 | 20 | #[error("numbers: {0}")] 21 | Numbers(#[from] ParseIntError), 22 | 23 | #[error("invalid disk")] 24 | InvalidDisk, 25 | 26 | #[error("superblock: {0}")] 27 | Superblock(#[from] superblock::Error), 28 | } 29 | 30 | mod disks; 31 | pub use disks::Disk; 32 | mod partition; 33 | pub use disks::Kind as DiskKind; 34 | pub use partition::Kind as PartitionKind; 35 | pub use partition::Partition; 36 | 37 | pub use superblock::Kind as SuperblockKind; 38 | pub use superblock::Superblock; 39 | -------------------------------------------------------------------------------- /selections/gnome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gnome", 3 | "summary": "GNOME Desktop", 4 | "description": "Recommended for most users", 5 | "display": true, 6 | "priority": 10, 7 | "depends": [ 8 | "base-desktop" 9 | ], 10 | "required": [ 11 | "binary(celluloid)", 12 | "binary(file-roller)", 13 | "binary(gnome-calculator)", 14 | "binary(gnome-calendar)", 15 | "binary(gnome-clocks)", 16 | "binary(gnome-control-center)", 17 | "binary(gnome-disks)", 18 | "binary(gnome-software)", 19 | "binary(gnome-text-editor)", 20 | "binary(gnome-tweaks)", 21 | "binary(gnome-weather)", 22 | "binary(loupe)", 23 | "binary(nautilus)", 24 | "binary(papers)", 25 | "binary(ptyxis)", 26 | "binary(resources)", 27 | "binary(snapshot)", 28 | "binary(starship)", 29 | "gnome-backgrounds", 30 | "loupe", 31 | "pkgset-aeryn-gnome-minimal", 32 | "pkgset-aeryn-gnome-recommended", 33 | "serpent-gnome-defaults", 34 | "sysbinary(gdm)" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /crates/installer/src/steps/context.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Lichen step context 6 | 7 | use std::{ 8 | fmt::Debug, 9 | path::PathBuf, 10 | process::{Command, Output}, 11 | }; 12 | 13 | /// Context for the steps that are executing 14 | /// The context provides access to the core installation variables as 15 | /// well as simplified paths for executing commands in a consistent 16 | /// fashion. 17 | pub trait Context<'a>: Sized + Debug + Send { 18 | /// Return the root directory of the installation 19 | fn root(&'a self) -> &'a PathBuf; 20 | 21 | /// Run the command asynchronously via the context 22 | fn run_command(&self, cmd: &mut Command) -> Result<(), super::Error>; 23 | 24 | /// Run command, capture the output 25 | /// Accepts optional string to write as stdin 26 | fn run_command_captured(&self, cmd: &mut Command, input: Option<&str>) -> Result; 27 | } 28 | -------------------------------------------------------------------------------- /selections/cosmic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic", 3 | "summary": "COSMIC Desktop", 4 | "description": "Stable, but still under active development", 5 | "display": true, 6 | "priority": 30, 7 | "depends": ["base-desktop"], 8 | "required": [ 9 | "cosmic-applets", 10 | "cosmic-bg", 11 | "cosmic-comp", 12 | "cosmic-desktop", 13 | "cosmic-edit", 14 | "cosmic-files", 15 | "cosmic-greeter", 16 | "cosmic-icons", 17 | "cosmic-launcher", 18 | "cosmic-notifications", 19 | "cosmic-osd", 20 | "cosmic-panel", 21 | "cosmic-player", 22 | "cosmic-randr", 23 | "cosmic-screenshot", 24 | "cosmic-session", 25 | "cosmic-settings", 26 | "cosmic-settings-daemon", 27 | "cosmic-store", 28 | "cosmic-term", 29 | "cosmic-wallpapers", 30 | "cosmic-workspaces", 31 | "font-opensans", 32 | "pkgset-aeryn-cosmic-minimal", 33 | "pkgset-aeryn-cosmic-recommended", 34 | "serpent-artwork", 35 | "xdg-desktop-portal-cosmic", 36 | "xdg-desktop-portal-gtk" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '30 2 * * *' 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Build & Test Project 18 | 19 | steps: 20 | - name: Checkout source 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Rust 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt, clippy 27 | 28 | - name: Check Formatting 29 | run: cargo fmt --all -- --check 30 | 31 | - name: Cargo Cache 32 | uses: Swatinem/rust-cache@v2 33 | 34 | - name: Build project 35 | run: cargo build 36 | 37 | - name: Test project 38 | run: cargo test --workspace 39 | 40 | - name: Run clippy 41 | uses: giraffate/clippy-action@v1 42 | with: 43 | reporter: 'github-pr-check' 44 | clippy_flags: --workspace --no-deps 45 | filter_mode: nofilter 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lichen 2 | 3 | ## Overview 4 | An experimental text based installer run from the command line to install AerynOS. 5 | 6 | `lichen` has been developed to a minimum viable level to allow for installing AerynOS by experienced users. Prior to installation, users will need to format their disk with a GPT partition table with an esp partition, xbootldr partition and a root partition. Instructions for this can be found on our documentation [website](https://aerynos.dev/aerynos/faq/). 7 | 8 | The AerynOS team are currently focusing on [infrastructure](https://github.com/AerynOS/infra) and [os-tooling](https://github.com/AerynOS/os-tools) development so `lichen` is currently in maintenance mode. There are however eventual plans to build upon `lichen` to provide a more user-friendly experience including a graphical user interface. In the meantime, we are still accepting contributions to help improve the codebase. 9 | 10 | ## Build & Test 11 | 12 | cargo build 13 | sudo ./target/debug/lichen 14 | 15 | To quit the installer, press `ESC` to switch to command mode, then press `q`. 16 | 17 | ## License 18 | 19 | `lichen` is available under the terms of the [MPL-2.0](https://spdx.org/licenses/MPL-2.0.html) 20 | -------------------------------------------------------------------------------- /lichen_cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | error::Error, 4 | fs::{self, File}, 5 | io::Write, 6 | path::Path, 7 | }; 8 | 9 | const SELECTIONS_DIR: &str = "../selections"; 10 | 11 | fn main() -> Result<(), Box> { 12 | println!("cargo::rerun-if-changed={SELECTIONS_DIR}"); 13 | 14 | let out_dir = env::var("OUT_DIR")?; 15 | let dest_path = Path::new(&out_dir).join("selections.rs"); 16 | 17 | let mut selections = File::create(&dest_path)?; 18 | 19 | writeln!(&mut selections, r##"macro_rules! selections {{"##,)?; 20 | writeln!(&mut selections, r##"() => {{["##,)?; 21 | for f in fs::read_dir(SELECTIONS_DIR)? { 22 | let f = f?; 23 | 24 | if !f.file_type()?.is_file() { 25 | continue; 26 | } 27 | 28 | if !f.file_name().to_str().unwrap().ends_with(".json") { 29 | continue; 30 | } 31 | 32 | writeln!( 33 | &mut selections, 34 | r##"Group::from_str(include_str!("../{name}"))?,"##, 35 | name = f.path().display(), 36 | )?; 37 | } 38 | 39 | writeln!(&mut selections, r##"]}};"##,)?; 40 | writeln!(&mut selections, r##"}}"##,)?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /selections/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base", 3 | "summary": "Base system", 4 | "description": "The base system for all AerynOS installations.", 5 | "required": [ 6 | "bash-completion", 7 | "binary(bash)", 8 | "binary(dash)", 9 | "binary(file)", 10 | "binary(find)", 11 | "binary(gawk)", 12 | "binary(grep)", 13 | "binary(gzip)", 14 | "binary(less)", 15 | "binary(moss)", 16 | "binary(nano)", 17 | "binary(ping)", 18 | "binary(ps)", 19 | "binary(python)", 20 | "binary(screen)", 21 | "binary(sed)", 22 | "binary(setfont)", 23 | "binary(ssh)", 24 | "binary(sudo)", 25 | "binary(tar)", 26 | "binary(unzip)", 27 | "binary(wget)", 28 | "binary(which)", 29 | "sysbinary(fsck.ext4)", 30 | "sysbinary(fsck.f2fs)", 31 | "sysbinary(fsck.xfs)", 32 | "ca-certificates", 33 | "uutils-coreutils", 34 | "dbus", 35 | "dbus-broker", 36 | "dmidecode", 37 | "iproute2", 38 | "hyperv-daemons", 39 | "pciutils", 40 | "pkgset-aeryn-base", 41 | "pkgset-aeryn-utilities", 42 | "pkgset-aeryn-virtual", 43 | "qemu-guest-agent", 44 | "usbutils", 45 | "shadow", 46 | "spice-vdagent", 47 | "systemd", 48 | "tzdata", 49 | "util-linux" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | "lichen_cli", 5 | # This is unused 6 | # "lichen_ipc" 7 | ] 8 | default-members = [ 9 | "lichen_cli" 10 | ] 11 | resolver = "2" 12 | 13 | [workspace.dependencies] 14 | bitflags = "2.6.0" 15 | chrono-tz = "0.10.0" 16 | color-eyre = { version = "0.6.3", features = ["issue-url"] } 17 | crossterm = { version = "0.29.0", features = ["serde", "event-stream"] } 18 | env_logger = "0.11.5" 19 | fs-err = { version = "3.0.0", features = ["tokio"] } 20 | human_bytes = "0.4.3" 21 | itertools = "0.14.0" 22 | libc = "0.2.155" 23 | log = "0.4.22" 24 | nix = "0.30.1" 25 | serde = { version = "1.0.204", features = ["derive"] } 26 | serde_derive = "1.0.204" 27 | serde_json = "1.0.120" 28 | superblock = { git = "https://github.com/AerynOS/disks-rs.git", rev = "0768fe553b123b2086980bc809011e9786bffd95" } 29 | thiserror = "2.0.3" 30 | topology = { git = "https://github.com/AerynOS/blsforme.git", rev = "a09fa783aab22c8a057fc38f827d2fd2d3d157d9" } 31 | varlink = { version = "11.0.1" } 32 | varlink_generator = { version = "10.1.0 "} 33 | 34 | [workspace.lints.rust] 35 | rust_2018_idioms = { level = "warn", priority = -1 } 36 | semicolon_in_expressions_from_macros = "warn" 37 | unused_import_braces = "warn" 38 | unused_qualifications = "warn" 39 | -------------------------------------------------------------------------------- /crates/system/src/locale/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | use std::fmt; 6 | 7 | use thiserror::Error; 8 | 9 | /// Locale joins Territory + Language 10 | #[derive(Debug)] 11 | pub struct Locale<'a> { 12 | pub name: String, 13 | pub display_name: String, 14 | pub language: &'a Language, 15 | pub territory: &'a Territory, 16 | pub modifier: Option, 17 | pub codeset: Option, 18 | } 19 | 20 | impl fmt::Display for Locale<'_> { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | f.write_str(&self.display_name) 23 | } 24 | } 25 | 26 | /// Sane representation for UI purposes 27 | #[derive(PartialEq, Eq, Debug)] 28 | pub struct Territory { 29 | pub code: String, 30 | pub code2: String, 31 | pub display_name: String, 32 | pub flag: String, 33 | } 34 | 35 | /// Simplistic language representation 36 | #[derive(PartialEq, Eq, Debug)] 37 | pub struct Language { 38 | pub code: String, 39 | pub code2: Option, 40 | pub display_name: String, 41 | pub inverted_name: Option, 42 | } 43 | 44 | #[derive(Debug, Error)] 45 | pub enum Error { 46 | #[error("io: {0}")] 47 | IO(#[from] std::io::Error), 48 | 49 | #[error("json parsing error: {0}")] 50 | Serde(#[from] serde_json::Error), 51 | } 52 | 53 | mod iso_3166; 54 | mod iso_639_2; 55 | mod iso_639_3; 56 | 57 | mod registry; 58 | pub use registry::Registry; 59 | -------------------------------------------------------------------------------- /lichen_ipc/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | use std::env; 6 | 7 | use clap::Parser; 8 | use color_eyre::eyre::bail; 9 | use lichen_ipc::{disks, disks_ipc}; 10 | use pretty_env_logger::formatted_builder; 11 | use varlink::VarlinkService; 12 | 13 | /// Command line arguments parser 14 | #[derive(Parser)] 15 | struct Cli { 16 | /// Varlink socket address 17 | #[clap(long)] 18 | varlink: Option, 19 | } 20 | 21 | fn main() -> color_eyre::Result<()> { 22 | formatted_builder() 23 | .filter_level(log::LevelFilter::Info) 24 | .parse_default_env() 25 | .init(); 26 | 27 | color_eyre::install().unwrap(); 28 | 29 | let args = Cli::parse(); 30 | let socket = if let Some(varlink) = args.varlink { 31 | varlink 32 | } else if let Ok(varlink_address) = env::var("VARLINK_ADDRESS") { 33 | varlink_address 34 | } else { 35 | bail!("Usage: lichen-ipc --varlink "); 36 | }; 37 | 38 | // bind our interfaces to the varlink service 39 | let interface = disks_ipc::new(Box::new(disks::Service::new())); 40 | let service = VarlinkService::new( 41 | "Serpent OS", 42 | "Lichen Installer", 43 | "0.1", 44 | "https://serpentos.com/", 45 | vec![Box::new(interface)], 46 | ); 47 | 48 | log::info!("lichen-ipc now listening on {socket}"); 49 | 50 | varlink::listen( 51 | service, 52 | &socket, 53 | &varlink::ListenConfig { 54 | idle_timeout: 0, 55 | ..Default::default() 56 | }, 57 | )?; 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /crates/installer/src/steps/cleanup.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Lichen cleanup steps 6 | //! Despite the fact we could trivially implement as a `Drop` or some 7 | //! other Rust idiom, we still need to provide active feedback to the user 8 | //! for whatever step is currently running. 9 | //! 10 | //! To that effect we provide a mirror of [`Step`] by way of a Cleanup. 11 | 12 | use super::{partitions, Context}; 13 | 14 | /// Encapsulate the cleanup stages 15 | pub enum Cleanup { 16 | /// Unmount a mountpoint 17 | Unmount(Box), 18 | 19 | /// Sync filesystems pre unmount 20 | Sync(Box), 21 | } 22 | 23 | impl<'a> Cleanup { 24 | /// Create new unmount cleanup stage 25 | pub fn unmount(unmount: partitions::Unmount) -> Self { 26 | Self::Unmount(Box::new(unmount)) 27 | } 28 | 29 | /// Create new sync helper 30 | pub fn sync_fs() -> Self { 31 | Self::Sync(Box::new(partitions::SyncFS {})) 32 | } 33 | 34 | /// Return cleanup step title 35 | pub fn title(&self) -> String { 36 | match &self { 37 | Self::Unmount(s) => s.title(), 38 | Self::Sync(s) => s.title(), 39 | } 40 | } 41 | 42 | /// Fully describe cleanup step 43 | pub fn describe(&self) -> String { 44 | match &self { 45 | Self::Unmount(s) => s.describe(), 46 | Self::Sync(s) => s.describe(), 47 | } 48 | } 49 | 50 | /// Execute the cleanup step 51 | pub fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 52 | match &self { 53 | Self::Unmount(s) => Ok(s.execute(context)?), 54 | Self::Sync(s) => Ok(s.execute(context)?), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/installer/src/steps/packaging.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Package management encapsulation (moss only) 6 | use std::process::Command; 7 | 8 | use super::Context; 9 | 10 | /// Add a repository to the target disk 11 | #[derive(Debug)] 12 | pub struct AddRepo { 13 | pub(crate) uri: String, 14 | pub(crate) name: String, 15 | pub(crate) priority: u64, 16 | } 17 | 18 | impl<'a> AddRepo { 19 | /// Basic display title 20 | pub(super) fn title(&self) -> String { 21 | format!("Add repo {}", self.name) 22 | } 23 | 24 | /// Render the action 25 | pub(super) fn describe(&self) -> String { 26 | format!("{} (priority {})", self.uri, self.priority) 27 | } 28 | 29 | /// Run moss against the target, adding a repo 30 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), super::Error> { 31 | let mut cmd = Command::new("moss"); 32 | cmd.arg("-D"); 33 | cmd.arg(context.root()); 34 | cmd.args(["repo", "add", &self.name, &self.uri, "-p"]); 35 | cmd.arg(self.priority.to_string()); 36 | cmd.arg("-y"); 37 | 38 | // Run 39 | context.run_command(&mut cmd) 40 | } 41 | } 42 | 43 | /// Install packages to destdir 44 | #[derive(Debug)] 45 | pub struct InstallPackages { 46 | pub(crate) names: Vec, 47 | } 48 | 49 | impl<'a> InstallPackages { 50 | /// Basic display title 51 | pub(super) fn title(&self) -> String { 52 | "Install".into() 53 | } 54 | 55 | /// Render the action 56 | pub(super) fn describe(&self) -> String { 57 | "packages to sysroot".into() 58 | } 59 | 60 | /// Run moss against the target, adding a repo 61 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), super::Error> { 62 | let mut cmd = Command::new("moss"); 63 | cmd.arg("-D"); 64 | cmd.arg(context.root()); 65 | cmd.arg("install"); 66 | cmd.args(&self.names); 67 | cmd.arg("-y"); 68 | 69 | context.run_command(&mut cmd) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lichen_ipc/README.md: -------------------------------------------------------------------------------- 1 | # lichen_ipc 2 | 3 | This crate contains the `lichen_ipc` executable, a helper backend for the lichen 4 | installer that communicates using the [varlink](https://varlink.org) protocol over 5 | a local UNIX socket. 6 | 7 | It is intended to be launched by a compatible frontend via some escalation system, such 8 | as `pkexec` or `sudo`, ensuring that the frontend can communicate with the backend without 9 | running as root itself. 10 | 11 | It is in a very early stage, but eventually will be the entire backend of our installer. 12 | 13 | ## Testing 14 | 15 | First, launch the backend: 16 | 17 | ```sh 18 | pkexec ./target/debug/lichen_ipc --varlink=unix:@testinglichen 19 | ``` 20 | 21 | Then, in another terminal, run the [varlink cli](https://crates.io/crates/varlink-cli) 22 | 23 | ### Disk enumeration 24 | 25 | ```sh 26 | varlink call unix:@testinglichen/com.serpentos.lichen.disks.GetDisks 27 | ``` 28 | ```json 29 | { 30 | "disks": [ 31 | { 32 | "block_size": 512, 33 | "kind": "ssd", 34 | "model": "WD_BLACK SN770 1TB", 35 | "path": "/dev/nvme0n1", 36 | "size": 1953525168, 37 | "vendor": null 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | ### Partition enumeration 44 | 45 | ```sh 46 | varlink call unix:@testinglichen/com.serpentos.lichen.disks.GetPartitions '{"disk": "/dev/sda"}' 47 | ``` 48 | 49 | ```json 50 | { 51 | "partitions": [ 52 | { 53 | "kind": "esp", 54 | "path": "/dev/nvme0n1p1", 55 | "size": 134217728, 56 | "superblock_kind": "unknown", 57 | "uuid": "3a1338aa-d98b-45ef-b72b-62f9752ef2d2" 58 | }, 59 | { 60 | "kind": "xbootldr", 61 | "path": "/dev/nvme0n1p2", 62 | "size": 1073741824, 63 | "superblock_kind": "unknown", 64 | "uuid": "915127ba-acd8-45db-a865-a9d8329d6f26" 65 | }, 66 | { 67 | "kind": "regular", 68 | "path": "/dev/nvme0n1p3", 69 | "size": 4294967296, 70 | "superblock_kind": "unknown", 71 | "uuid": "2b89b961-4145-498f-8fb7-7b3178905c75" 72 | }, 73 | { 74 | "kind": "regular", 75 | "path": "/dev/nvme0n1p4", 76 | "size": 994700165120, 77 | "superblock_kind": "ext4", 78 | "uuid": "e1ee771f-0c94-45fe-b544-2d22e50784e0" 79 | } 80 | ] 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /crates/system/src/disk/partition.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Partition APIs 6 | 7 | use std::fmt::Display; 8 | use std::io::{Cursor, Read}; 9 | use std::path::PathBuf; 10 | 11 | use fs_err as fs; 12 | use gpt::disk::LogicalBlockSize; 13 | use gpt::partition_types; 14 | use superblock::Superblock; 15 | 16 | /// Partition on a GPT disk 17 | #[derive(Debug, Clone, Default)] 18 | pub struct Partition { 19 | pub path: PathBuf, 20 | pub kind: Kind, 21 | pub size: u64, 22 | pub uuid: String, 23 | pub sb: Option, 24 | } 25 | 26 | impl Display for Partition { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | f.write_str(&self.path.display().to_string()) 29 | } 30 | } 31 | /// Specialised type of partition 32 | #[derive(Debug, Clone, Default)] 33 | #[allow(clippy::upper_case_acronyms)] 34 | pub enum Kind { 35 | ESP, 36 | XBOOTLDR, 37 | #[default] 38 | Regular, 39 | } 40 | 41 | /// Superblock scanning, self contained 42 | fn scan_superblock(path: &PathBuf) -> Result { 43 | let fi = fs::File::open(path)?; 44 | let mut buffer: Vec = Vec::with_capacity(2 * 1024 * 1024); 45 | fi.take(2 * 1024 * 1024).read_to_end(&mut buffer)?; 46 | let mut cursor = Cursor::new(&buffer); 47 | let sb = Superblock::from_reader(&mut cursor)?; 48 | Ok(sb.kind()) 49 | } 50 | 51 | impl Partition { 52 | /// Construct new Partition from the given GPT Partition and block size 53 | pub fn from(value: &gpt::partition::Partition, block_size: &LogicalBlockSize) -> Result { 54 | let uuid = value.part_guid.hyphenated().to_string(); 55 | let path = fs::canonicalize(format!("/dev/disk/by-partuuid/{uuid}"))?; 56 | let kind = match value.part_type_guid { 57 | partition_types::EFI => Kind::ESP, 58 | partition_types::FREEDESK_BOOT => Kind::XBOOTLDR, 59 | _ => Kind::Regular, 60 | }; 61 | let sb = scan_superblock(&path).ok(); 62 | let size = value.bytes_len(*block_size)?; 63 | Ok(Self { 64 | path, 65 | kind, 66 | size, 67 | uuid, 68 | sb, 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/installer/src/partitions.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! A higher abstraction over partitions for the purposes of 6 | //! installer usage. 7 | //! Quite simply we only care about the difference in a regular 8 | //! partition, and a boot partition. 9 | 10 | use std::fmt::Display; 11 | 12 | use human_bytes::human_bytes; 13 | use system::disk; 14 | 15 | /// A boot partition is an EFI System Partition which may or may 16 | /// not be paired with an `XBOOTLDR` partition, relative to its location 17 | /// on the same GPT disk. 18 | /// This is a requirement per the Boot Loader Specification. 19 | #[derive(Debug, Clone)] 20 | pub struct BootPartition { 21 | pub(crate) esp: disk::Partition, 22 | pub(crate) xbootldr: Option, 23 | pub(crate) parent_desc: String, 24 | } 25 | 26 | impl Display for BootPartition { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | let opt_string = if let Some(xbootldr) = self.xbootldr.as_ref() { 29 | format!( 30 | "with XBOOTLDR {} ({}) ", 31 | xbootldr.path.display(), 32 | human_bytes(xbootldr.size as f64) 33 | ) 34 | } else { 35 | "".into() 36 | }; 37 | f.write_fmt(format_args!( 38 | "{} ({}) {}[on {}]", 39 | self.esp.path.display(), 40 | human_bytes(self.esp.size as f64), 41 | opt_string, 42 | self.parent_desc 43 | )) 44 | } 45 | } 46 | 47 | /// A system partition is simply a regular partition with a specified mountpoint 48 | /// within the root. 49 | #[derive(Debug, Clone)] 50 | pub struct SystemPartition { 51 | pub(crate) partition: disk::Partition, 52 | 53 | /// Where will it be mounted 54 | pub mountpoint: Option, 55 | 56 | pub(crate) parent_desc: String, 57 | } 58 | 59 | impl Display for SystemPartition { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | f.write_fmt(format_args!( 62 | "{} ({}) [on {}]", 63 | self.partition.path.display(), 64 | human_bytes(self.partition.size as f64), 65 | self.parent_desc 66 | )) 67 | } 68 | } 69 | 70 | impl AsRef for SystemPartition { 71 | fn as_ref(&self) -> &disk::Partition { 72 | &self.partition 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/system/src/locale/iso_639_2.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Parsing for ISO-639 files from iso-codes 6 | //! Essentially, loading of languages 7 | 8 | use serde::Deserialize; 9 | 10 | use super::Language; 11 | 12 | /// JSON document for ISO-639-2 13 | #[derive(Deserialize)] 14 | pub struct Document<'a> { 15 | #[serde(rename = "639-2", borrow)] 16 | pub entries: Vec>, 17 | } 18 | 19 | /// A single entry in the JSON document 20 | #[derive(Deserialize)] 21 | pub struct Entry<'a> { 22 | #[serde(rename = "alpha_2", borrow)] 23 | pub code2: Option<&'a str>, 24 | 25 | #[serde(rename = "alpha_3", borrow)] 26 | pub code3: &'a str, 27 | 28 | /// Official display name 29 | #[serde(borrow)] 30 | pub name: &'a str, 31 | 32 | /// Common name (optional) 33 | #[serde(borrow)] 34 | pub common_name: Option<&'a str>, 35 | 36 | /// Three letter bibliographic 37 | pub bibliographic: Option<&'a str>, 38 | } 39 | 40 | impl From<&Entry<'_>> for Language { 41 | /// Convert iso entry into Language 42 | fn from(value: &Entry<'_>) -> Self { 43 | Self { 44 | code: value.code3.into(), 45 | code2: value.code2.map(|v| v.into()), 46 | display_name: value.name.into(), 47 | inverted_name: None, 48 | } 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::Document; 55 | 56 | #[test] 57 | fn load_iso_639_2() { 58 | const TEST_DATA: &str = r#" 59 | { 60 | "639-2": [ 61 | { 62 | "alpha_2": "gd", 63 | "alpha_3": "gla", 64 | "name": "Gaelic; Scottish Gaelic" 65 | }, 66 | { 67 | "alpha_2": "ga", 68 | "alpha_3": "gle", 69 | "name": "Irish" 70 | }, 71 | { 72 | "alpha_2": "gl", 73 | "alpha_3": "glg", 74 | "name": "Galician" 75 | } 76 | ] 77 | } 78 | "#; 79 | 80 | let loaded = serde_json::from_str::>(TEST_DATA).expect("Failed to decode ISO-639-2 data"); 81 | let ga = loaded 82 | .entries 83 | .iter() 84 | .find(|i| i.code3 == "gle") 85 | .expect("Failed to find GLE"); 86 | assert_eq!(ga.name, "Irish"); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/installer/src/account.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /// Identifies an account 6 | #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] 7 | pub struct Account { 8 | /// User ID 9 | pub uid: libc::uid_t, 10 | 11 | /// Group ID 12 | pub gid: libc::gid_t, 13 | 14 | /// Account name 15 | pub username: String, 16 | 17 | /// Human username string 18 | pub gecos: Option, 19 | 20 | /// Home directory 21 | pub homedir: String, 22 | 23 | /// Which shell to use 24 | pub shell: String, 25 | 26 | /// New password 27 | pub password: Option, 28 | 29 | /// Builtin user? (root) 30 | pub builtin: bool, 31 | } 32 | 33 | impl Default for Account { 34 | fn default() -> Self { 35 | Self { 36 | uid: 1000, 37 | gid: 1000, 38 | username: "user".into(), 39 | gecos: None, 40 | homedir: "/home/user".into(), 41 | shell: "/bin/bash".into(), 42 | password: None, 43 | builtin: false, 44 | } 45 | } 46 | } 47 | 48 | impl Account { 49 | /// Return an account definition for the root account 50 | pub fn root() -> Self { 51 | Self { 52 | uid: 0, 53 | gid: 0, 54 | username: "root".to_string(), 55 | homedir: "/root".to_string(), 56 | builtin: true, 57 | ..Default::default() 58 | } 59 | } 60 | 61 | /// New account with the given username 62 | pub fn new>(username: S) -> Self { 63 | Self { 64 | username: username.as_ref().to_string(), 65 | ..Default::default() 66 | } 67 | } 68 | 69 | /// Update the IDs 70 | pub fn with_id(self, uid: libc::uid_t, gid: libc::gid_t) -> Self { 71 | Self { uid, gid, ..self } 72 | } 73 | 74 | /// Update the gecos 75 | pub fn with_gecos>(self, gecos: S) -> Self { 76 | Self { 77 | gecos: Some(gecos.as_ref().to_string()), 78 | ..self 79 | } 80 | } 81 | 82 | /// Update the shell 83 | pub fn with_shell>(self, shell: S) -> Self { 84 | Self { 85 | shell: shell.as_ref().to_string(), 86 | ..self 87 | } 88 | } 89 | 90 | /// Update the password 91 | pub fn with_password>(self, p: P) -> Self { 92 | Self { 93 | password: Some(p.as_ref().to_string()), 94 | ..self 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/system/src/locale/iso_3166.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Parsing for ISO-3166-1 files from iso-codes 6 | //! Essentially, loading of territories. 7 | 8 | use serde::Deserialize; 9 | 10 | use super::Territory; 11 | 12 | /// Wrap the document stream from JSON into referenced 13 | /// entries in the input text 14 | #[derive(Deserialize)] 15 | pub struct Document<'a> { 16 | #[serde(rename = "3166-1", borrow)] 17 | pub entries: Vec>, 18 | } 19 | 20 | /// Maps an entry from iso-codes to a Rusty struct. 21 | #[derive(Deserialize)] 22 | pub struct Entry<'a> { 23 | /// Two-element code identifying the entry 24 | #[serde(rename = "alpha_2", borrow)] 25 | pub code2: &'a str, 26 | 27 | /// Three-element code identifying the entry 28 | #[serde(rename = "alpha_3", borrow)] 29 | pub code3: &'a str, 30 | 31 | /// Unicode flag representation 32 | #[serde(borrow)] 33 | pub flag: &'a str, 34 | 35 | /// Normalised name 36 | #[serde(borrow)] 37 | pub name: &'a str, 38 | 39 | /// Unique territory 40 | #[serde(borrow)] 41 | pub numeric: &'a str, 42 | 43 | /// Formal name if present 44 | #[serde(borrow)] 45 | pub official_name: Option<&'a str>, 46 | } 47 | 48 | impl From<&Entry<'_>> for Territory { 49 | fn from(value: &Entry<'_>) -> Self { 50 | if let Some(display) = value.official_name { 51 | Self { 52 | code: value.code3.into(), 53 | code2: value.code2.into(), 54 | display_name: display.into(), 55 | flag: value.flag.into(), 56 | } 57 | } else { 58 | Self { 59 | code: value.code3.into(), 60 | code2: value.code2.into(), 61 | display_name: value.name.into(), 62 | flag: value.flag.into(), 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::Document; 71 | 72 | #[test] 73 | fn basic_load() { 74 | const TEST_DATA: &str = r#" 75 | { 76 | "3166-1": [ 77 | 78 | { 79 | "alpha_2": "IN", 80 | "alpha_3": "IND", 81 | "flag": "🇮🇳", 82 | "name": "India", 83 | "numeric": "356", 84 | "official_name": "Republic of India" 85 | }, 86 | { 87 | "alpha_2": "IO", 88 | "alpha_3": "IOT", 89 | "flag": "🇮🇴", 90 | "name": "British Indian Ocean Territory", 91 | "numeric": "086" 92 | }, 93 | { 94 | "alpha_2": "IE", 95 | "alpha_3": "IRL", 96 | "flag": "🇮🇪", 97 | "name": "Ireland", 98 | "numeric": "372" 99 | } 100 | ] 101 | } 102 | "#; 103 | let loaded = serde_json::from_str::>(TEST_DATA).expect("Failed to decode ISO-3166 JSON"); 104 | 105 | let ie = loaded 106 | .entries 107 | .iter() 108 | .find(|e| e.code3 == "IRL") 109 | .expect("Failed to find locale"); 110 | assert_eq!(ie.name, "Ireland"); 111 | eprintln!("Ireland: {}", ie.flag); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /crates/system/src/locale/iso_639_3.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Parsing for ISO-639-3 JSON files 6 | use serde::Deserialize; 7 | 8 | use super::Language; 9 | 10 | /// JSON document for 639-3 11 | #[derive(Deserialize)] 12 | pub struct Document<'a> { 13 | #[serde(rename = "639-3", borrow)] 14 | pub entries: Vec>, 15 | } 16 | 17 | /// Language scope 18 | #[derive(Deserialize)] 19 | pub enum Scope { 20 | #[serde(rename = "I")] 21 | Individual, 22 | 23 | #[serde(rename = "M")] 24 | Macrolanguage, 25 | 26 | #[serde(rename = "S")] 27 | Special, 28 | } 29 | 30 | #[derive(Deserialize)] 31 | pub enum Kind { 32 | #[serde(rename = "A")] 33 | Ancient, 34 | #[serde(rename = "C")] 35 | Constructed, 36 | #[serde(rename = "E")] 37 | Extinct, 38 | #[serde(rename = "H")] 39 | Historical, 40 | #[serde(rename = "L")] 41 | Living, 42 | #[serde(rename = "S")] 43 | Special, 44 | } 45 | 46 | /// Single entry in the JSON document 47 | #[derive(Deserialize)] 48 | pub struct Entry<'a> { 49 | /// Three letter code 50 | #[serde(rename = "alpha_3", borrow)] 51 | pub code: &'a str, 52 | 53 | /// Sometimes a 2 letter code is present 54 | #[serde(rename = "alpha_2", borrow)] 55 | pub code2: Option<&'a str>, 56 | 57 | /// Official name 58 | #[serde(borrow)] 59 | pub name: &'a str, 60 | 61 | /// Inverted name 62 | #[serde(borrow)] 63 | pub inverted_name: Option<&'a str>, 64 | 65 | /// Scope of the language 66 | pub scope: Scope, 67 | 68 | /// Type of language 69 | #[serde(rename = "type")] 70 | pub kind: Kind, 71 | 72 | /// Three letter bibliographic 73 | pub bibliographic: Option<&'a str>, 74 | 75 | /// Common name (optional) 76 | #[serde(borrow)] 77 | pub common_name: Option<&'a str>, 78 | } 79 | 80 | impl From<&Entry<'_>> for Language { 81 | fn from(value: &Entry<'_>) -> Self { 82 | let display = if let Some(name) = value.common_name { 83 | name.into() 84 | } else { 85 | value.name.into() 86 | }; 87 | Self { 88 | code: value.code.into(), 89 | code2: value.code2.map(|v| v.into()), 90 | display_name: display, 91 | inverted_name: value.inverted_name.map(|v| v.into()), 92 | } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::{Document, Kind, Scope}; 99 | 100 | #[test] 101 | fn load_iso_639_3() { 102 | const TEST_DATA: &str = r#" 103 | { 104 | "639-3": [ 105 | { 106 | "alpha_3": "gld", 107 | "name": "Nanai", 108 | "scope": "I", 109 | "type": "L" 110 | }, 111 | { 112 | "alpha_2": "ga", 113 | "alpha_3": "gle", 114 | "name": "Irish", 115 | "scope": "I", 116 | "type": "L" 117 | }, 118 | { 119 | "alpha_2": "gl", 120 | "alpha_3": "glg", 121 | "name": "Galician", 122 | "scope": "I", 123 | "type": "L" 124 | } 125 | ] 126 | } 127 | "#; 128 | 129 | let loaded = serde_json::from_str::>(TEST_DATA).expect("Failed to decode ISO-639-3 data"); 130 | let ga = loaded 131 | .entries 132 | .iter() 133 | .find(|i| i.code == "gle") 134 | .expect("Failed to find GLE"); 135 | assert!(matches!(ga.scope, Scope::Individual)); 136 | assert!(matches!(ga.kind, Kind::Living)); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/system/src/disk/disks.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Disk management 6 | 7 | use std::{ 8 | fmt::Display, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | use fs_err as fs; 13 | use gpt::GptConfig; 14 | 15 | use super::{Error, Partition}; 16 | 17 | /// Indicates type of disk device 18 | #[derive(Debug)] 19 | #[allow(clippy::upper_case_acronyms)] 20 | pub enum Kind { 21 | /// Hard disk drive 22 | HDD, 23 | 24 | /// Solid State device 25 | SSD, 26 | } 27 | 28 | impl Display for Kind { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | match &self { 31 | Kind::HDD => f.write_str("HDD"), 32 | Kind::SSD => f.write_str("SSD"), 33 | } 34 | } 35 | } 36 | 37 | /// Basic physical device mapping 38 | #[derive(Debug)] 39 | pub struct Disk { 40 | pub path: PathBuf, 41 | pub kind: Kind, 42 | pub model: Option, 43 | pub vendor: Option, 44 | pub block_size: u64, 45 | pub size: u64, 46 | } 47 | 48 | impl Display for Disk { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | let vendor = self.vendor.as_ref().map(|v| format!("{v} ")).unwrap_or_default(); 51 | 52 | let description = if let Some(model) = self.model.as_ref() { 53 | format!("{vendor}{model}") 54 | } else { 55 | vendor.to_string() 56 | }; 57 | 58 | f.write_fmt(format_args!("{} ({})", description, &self.kind)) 59 | } 60 | } 61 | 62 | impl Disk { 63 | /// Build a Disk from the given sysfs path 64 | pub fn from_sysfs_path(path: impl AsRef) -> Result { 65 | let path = path.as_ref(); 66 | let device_link = path.join("device"); 67 | let slavedir = path.join("slaves"); 68 | 69 | let file_name = path.file_name().ok_or(Error::InvalidDisk)?; 70 | 71 | // Ensure the device link is present (no virtual ram0 device, etc) 72 | if !device_link.exists() { 73 | return Err(Error::InvalidDisk); 74 | } 75 | 76 | // Root level devices, not interested in child partitions as yet. 77 | let ancestors = fs::read_dir(slavedir)?.filter_map(|m| m.ok()).collect::>(); 78 | if !ancestors.is_empty() { 79 | return Err(Error::InvalidDisk); 80 | } 81 | 82 | // SSD or HDD? 83 | let rotational = path.join("queue").join("rotational"); 84 | let kind = if rotational.exists() { 85 | match str::parse::(fs::read_to_string(rotational)?.trim())? { 86 | 0 => Kind::SSD, 87 | _ => Kind::HDD, 88 | } 89 | } else { 90 | Kind::HDD 91 | }; 92 | 93 | // additioal metadata. 94 | 95 | let vendor = fs::read_to_string(device_link.join("vendor")) 96 | .ok() 97 | .map(|f| f.trim().to_string()); 98 | let model = fs::read_to_string(device_link.join("model")) 99 | .ok() 100 | .map(|f| f.trim().to_string()); 101 | let block_size = str::parse::(fs::read_to_string(path.join("queue").join("physical_block_size"))?.trim())?; 102 | let size = str::parse::(fs::read_to_string(path.join("size"))?.trim())?; 103 | 104 | let path = PathBuf::from("/dev").join(file_name); 105 | 106 | Ok(Self { 107 | path, 108 | kind, 109 | vendor, 110 | model, 111 | block_size, 112 | size, 113 | }) 114 | } 115 | 116 | /// Discover all disks on the system 117 | pub fn discover() -> Result, Error> { 118 | let disks = fs::read_dir("/sys/class/block")? 119 | .filter_map(|f| { 120 | let f = f.ok()?; 121 | Disk::from_sysfs_path(f.path()).ok() 122 | }) 123 | .collect::>(); 124 | Ok(disks) 125 | } 126 | 127 | /// Return all partitions on the disk if it is GPT 128 | pub fn partitions(&self) -> Result, Error> { 129 | let path = self.path.clone(); 130 | let path: &PathBuf = &path; 131 | let device = Box::new(fs::File::open(path)?); 132 | let table = GptConfig::default().writable(false).open_from_device(device)?; 133 | let block_size = table.logical_block_size(); 134 | let mut parts = vec![]; 135 | for (_, part) in table.partitions().iter() { 136 | parts.push(Partition::from(part, block_size)?) 137 | } 138 | Ok(parts) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /crates/installer/src/selections.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Deserialising of selections from JSON files 6 | 7 | use std::{ 8 | collections::{BTreeMap, BTreeSet}, 9 | fmt::Display, 10 | str::FromStr, 11 | }; 12 | 13 | use itertools::Itertools; 14 | use serde::Deserialize; 15 | use thiserror::Error; 16 | 17 | /// Selection handling errors 18 | #[derive(Debug, Error)] 19 | pub enum Error { 20 | #[error("serde: {0}")] 21 | Deserialize(#[from] serde_json::Error), 22 | 23 | #[error("unknown group")] 24 | UnknownGroup(String), 25 | } 26 | 27 | #[derive(Debug, Default, Deserialize)] 28 | pub struct Group { 29 | /// Simple list-selection name for this selection group 30 | pub name: String, 31 | 32 | /// User-visible summary for this selection group 33 | pub summary: String, 34 | 35 | /// User visible description for this selection group 36 | pub description: String, 37 | 38 | /// Whether or not to display the "group" as an installable option 39 | #[serde(default)] 40 | pub display: bool, 41 | 42 | /// Priority for sorting desktops (lower priorities are sorted before higher ones) 43 | #[serde(default)] 44 | pub priority: u8, 45 | 46 | /// Optionally a set of selection groups forming the basis of this one 47 | #[serde(default)] 48 | pub depends: Vec, 49 | 50 | /// A set of package names (moss-encoded) that form this selection 51 | pub required: Vec, 52 | } 53 | 54 | impl Display for Group { 55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 | f.write_str(&self.summary) 57 | } 58 | } 59 | 60 | impl FromStr for Group { 61 | type Err = Error; 62 | 63 | /// Encapsulate serde_json::from_str() 64 | fn from_str(s: &str) -> Result { 65 | let us = serde_json::from_str(s)?; 66 | Ok(us) 67 | } 68 | } 69 | 70 | /// Simple selections management 71 | #[derive(Default)] 72 | pub struct Manager { 73 | groups: BTreeMap, 74 | } 75 | 76 | impl Manager { 77 | /// convenience: new Manager 78 | pub fn new() -> Self { 79 | Self::default() 80 | } 81 | 82 | /// Take ownership of some groups 83 | pub fn with_groups>(self, groups: I) -> Self { 84 | Self { 85 | groups: groups 86 | .into_iter() 87 | .map(|g| (g.name.clone(), g)) 88 | .collect::>(), 89 | } 90 | } 91 | 92 | /// Add a group to the manager 93 | pub fn insert(&mut self, g: Group) { 94 | self.groups.insert(g.name.clone(), g); 95 | } 96 | 97 | /// Return an iterator of references to the groups 98 | pub fn groups(&self) -> impl Iterator { 99 | self.groups.values().sorted_by_key(|x| x.priority) 100 | } 101 | 102 | /// privatwly recurse for string deps 103 | fn get_deps(&self, name: &str) -> Result, Error> { 104 | let group = self.groups.get(name).ok_or_else(|| Error::UnknownGroup(name.into()))?; 105 | let mut depends = group.depends.clone(); 106 | 107 | // Recursively build parent deps 108 | for parent in group.depends.iter() { 109 | let parent_deps = self.get_deps(parent)?; 110 | depends.extend(parent_deps) 111 | } 112 | 113 | Ok(depends) 114 | } 115 | 116 | /// Given the selected IDs, what are the total selections? 117 | pub fn selections_with<'a, I: IntoIterator>(&'a self, ids: I) -> Result, Error> { 118 | let mut selected_ids = BTreeSet::new(); 119 | for item in ids.into_iter() { 120 | let deps = self.get_deps(item)?; 121 | selected_ids.extend(deps); 122 | selected_ids.insert(item.into()); 123 | } 124 | let core = selected_ids.into_iter().filter_map(|id| self.groups.get(&id)); 125 | Ok(core.flat_map(|g| g.required.clone()).collect::>()) 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use std::str::FromStr; 132 | 133 | use crate::selections::{Group, Manager}; 134 | 135 | #[test] 136 | fn test_decode() { 137 | let d = Group::from_str(include_str!("../../../selections/develop.json")).expect("Failed to decode base JSON"); 138 | let b = Group::from_str(include_str!("../../../selections/base.json")).expect("Failed to decode base JSON"); 139 | 140 | let manager = Manager::new().with_groups([d, b]); 141 | let pkgs_partial = manager.selections_with(["base"]).expect("Needed single set of data"); 142 | let pkgs = manager 143 | .selections_with(["develop"]) 144 | .expect("Needed empty set of base selections"); 145 | assert_eq!(pkgs_partial.len(), 44); 146 | assert_eq!(pkgs.len(), 48); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/installer/src/steps/partitions.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Partition formatting 6 | 7 | use std::{path::PathBuf, process::Command}; 8 | 9 | use fs_err as fs; 10 | use system::disk::Partition; 11 | 12 | use super::Context; 13 | 14 | /// Format a partition 15 | #[derive(Debug)] 16 | pub struct FormatPartition<'a> { 17 | /// What partition are we formatting 18 | pub(crate) partition: &'a Partition, 19 | 20 | /// What filesystem would you like it to have 21 | pub(crate) filesystem: String, 22 | } 23 | 24 | impl<'a> FormatPartition<'a> { 25 | pub(super) fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 26 | let fs = self.filesystem.to_lowercase(); 27 | let (exec, args) = match fs.as_str() { 28 | "ext4" => ("mkfs.ext4", ["-F", &self.partition.path.display().to_string()]), 29 | "xfs" => ("mkfs.xfs", ["-f", &self.partition.path.display().to_string()]), 30 | "f2fs" => ("mkfs.f2fs", ["-f", &self.partition.path.display().to_string()]), 31 | _ => unimplemented!(), 32 | }; 33 | log::info!("Formatting {} as {}", self.partition.path.display(), self.filesystem); 34 | log::trace!("Running: {exec:?} w/ {args:?}"); 35 | 36 | // For now we drop output, but we'll wire up stdout/stderr in context 37 | let mut cmd = Command::new(exec); 38 | cmd.args(args); 39 | let _ = context.run_command_captured(&mut cmd, None)?; 40 | Ok(()) 41 | } 42 | 43 | pub(super) fn title(&self) -> String { 44 | "Format partition".into() 45 | } 46 | 47 | pub(super) fn describe(&self) -> String { 48 | format!("{} as {}", self.partition.path.display(), self.filesystem) 49 | } 50 | } 51 | 52 | /// Mount a given partition 53 | #[derive(Debug)] 54 | pub struct MountPartition<'a> { 55 | /// Which partition? 56 | pub(crate) partition: &'a Partition, 57 | 58 | /// Where are we mounting it? 59 | pub(crate) mountpoint: PathBuf, 60 | } 61 | 62 | impl<'a> MountPartition<'a> { 63 | pub(super) fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 64 | log::info!( 65 | "Mounting {} to {}", 66 | self.partition.path.display(), 67 | self.mountpoint.display() 68 | ); 69 | 70 | // Ensure target exists 71 | fs::create_dir_all(&self.mountpoint)?; 72 | let source = self.partition.path.to_string_lossy().to_string(); 73 | let dest = self.mountpoint.to_string_lossy().to_string(); 74 | let mut cmd = Command::new("mount"); 75 | cmd.args([&source, &dest]); 76 | 77 | let _ = context.run_command_captured(&mut cmd, None)?; 78 | Ok(()) 79 | } 80 | 81 | pub(super) fn title(&self) -> String { 82 | "Mount filesystem".into() 83 | } 84 | 85 | pub(super) fn describe(&self) -> String { 86 | format!("{} as {}", self.partition.path.display(), self.mountpoint.display()) 87 | } 88 | } 89 | 90 | /// Bind mount a source dir into a target dir 91 | #[derive(Debug)] 92 | pub struct BindMount { 93 | /// The source directory 94 | pub(crate) source: PathBuf, 95 | 96 | /// Destination directory 97 | pub(crate) dest: PathBuf, 98 | } 99 | 100 | impl<'a> BindMount { 101 | pub(super) fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 102 | log::info!("Bind mounting {} to {}", self.source.display(), self.dest.display()); 103 | 104 | // Ensure target exists 105 | fs::create_dir_all(&self.dest)?; 106 | let source = self.source.to_string_lossy().to_string(); 107 | let dest = self.dest.to_string_lossy().to_string(); 108 | let mut cmd = Command::new("mount"); 109 | cmd.args(["--bind", &source, &dest]); 110 | 111 | let _ = context.run_command_captured(&mut cmd, None)?; 112 | Ok(()) 113 | } 114 | 115 | pub(super) fn title(&self) -> String { 116 | "Bind mount filesystem".into() 117 | } 118 | 119 | pub(super) fn describe(&self) -> String { 120 | format!("{} on {}", self.source.display(), self.dest.display()) 121 | } 122 | } 123 | 124 | /// Unmount a given mountpoint 125 | #[derive(Debug)] 126 | pub struct Unmount { 127 | pub(crate) mountpoint: PathBuf, 128 | } 129 | 130 | impl<'a> Unmount { 131 | pub(super) fn title(&self) -> String { 132 | "Unmount".to_string() 133 | } 134 | 135 | pub(super) fn describe(&self) -> String { 136 | format!("{}", &self.mountpoint.display()) 137 | } 138 | 139 | pub(super) fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 140 | log::info!("Unmounting {}", self.mountpoint.display()); 141 | 142 | let dest = self.mountpoint.to_string_lossy().to_string(); 143 | let mut cmd = Command::new("umount"); 144 | cmd.arg(dest); 145 | 146 | let _ = context.run_command_captured(&mut cmd, None)?; 147 | Ok(()) 148 | } 149 | } 150 | 151 | /// A cleanup helper that invokes `sync` 152 | pub struct SyncFS {} 153 | 154 | impl<'a> SyncFS { 155 | pub(super) fn title(&self) -> String { 156 | "Sync".into() 157 | } 158 | 159 | pub(super) fn describe(&self) -> String { 160 | "filesystems".into() 161 | } 162 | 163 | pub(super) fn execute(&self, context: &impl Context<'a>) -> Result<(), super::Error> { 164 | log::info!("Syncing filesystems"); 165 | 166 | let mut cmd = Command::new("sync"); 167 | let _ = context.run_command_captured(&mut cmd, None); 168 | Ok(()) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lichen_ipc/src/disks.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | use log::debug; 6 | 7 | use crate::disks_ipc; 8 | use std::collections::HashMap; 9 | use std::sync::{Arc, RwLock}; 10 | 11 | /// Disk Service struct for Lichen 12 | pub struct Service { 13 | disks_cache: Arc>>, 14 | parts_cache: Arc>>>, 15 | } 16 | 17 | impl Default for Service { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl Service { 24 | /// Creates a new instance of Service 25 | pub fn new() -> Self { 26 | Self { 27 | disks_cache: Arc::new(RwLock::new(Vec::new())), 28 | parts_cache: Arc::new(RwLock::new(HashMap::new())), 29 | } 30 | } 31 | } 32 | 33 | impl disks_ipc::VarlinkInterface for Service { 34 | /// Retrieves the list of disks 35 | fn get_disks(&self, call: &mut dyn disks_ipc::Call_GetDisks) -> varlink::Result<()> { 36 | if let Ok(read_cell) = self.disks_cache.read() { 37 | if !read_cell.is_empty() { 38 | debug!("restoring from disk cache"); 39 | call.reply(read_cell.clone())?; 40 | return Ok(()); 41 | } 42 | } 43 | 44 | // Acquire a lock 45 | let mut cell = match self.disks_cache.write() { 46 | Ok(c) => c, 47 | Err(_) => return call.reply_disk_error("Cannot lock cache".to_string()), 48 | }; 49 | 50 | match system::disk::Disk::discover() { 51 | Ok(disks) => { 52 | let ret = disks 53 | .iter() 54 | .map(|d| disks_ipc::Disk { 55 | kind: match d.kind { 56 | system::disk::DiskKind::HDD => disks_ipc::Disk_kind::hdd, 57 | system::disk::DiskKind::SSD => disks_ipc::Disk_kind::ssd, 58 | }, 59 | path: d.path.to_string_lossy().to_string(), 60 | model: d.model.clone(), 61 | vendor: d.vendor.clone(), 62 | size: d.size as i64, 63 | block_size: d.block_size as i64, 64 | }) 65 | .collect::>(); 66 | cell.extend(ret.clone()); 67 | call.reply(ret)?; 68 | } 69 | Err(e) => return call.reply_disk_error(e.to_string()), 70 | } 71 | 72 | Ok(()) 73 | } 74 | 75 | /// Retrieves the list of partitions for a given disk 76 | fn get_partitions(&self, call: &mut dyn disks_ipc::Call_GetPartitions, disk: String) -> varlink::Result<()> { 77 | if let Ok(read_cell) = self.parts_cache.read() { 78 | if let Some(cache) = read_cell.get(&disk) { 79 | debug!("restoring from partition cache"); 80 | call.reply(cache.clone())?; 81 | return Ok(()); 82 | } 83 | } 84 | 85 | // Not cached, so acquire write lock 86 | let mut cell = match self.parts_cache.write() { 87 | Ok(c) => c, 88 | Err(e) => return call.reply_disk_error(e.to_string()), 89 | }; 90 | 91 | match system::disk::Disk::from_sysfs_path(disk.replace("/dev/", "/sys/class/block/")) { 92 | Ok(disk) => match disk.partitions() { 93 | Ok(partitions) => { 94 | let res = partitions 95 | .iter() 96 | .map(|p| disks_ipc::Partition { 97 | path: p.path.to_string_lossy().to_string(), 98 | kind: match p.kind { 99 | system::disk::PartitionKind::ESP => disks_ipc::Partition_kind::esp, 100 | system::disk::PartitionKind::XBOOTLDR => disks_ipc::Partition_kind::xbootldr, 101 | system::disk::PartitionKind::Regular => disks_ipc::Partition_kind::regular, 102 | }, 103 | size: p.size as i64, 104 | uuid: p.uuid.clone(), 105 | superblock_kind: if let Some(sb) = p.sb.as_ref() { 106 | match sb { 107 | system::disk::SuperblockKind::Btrfs => disks_ipc::Partition_superblock_kind::btrfs, 108 | system::disk::SuperblockKind::Ext4 => disks_ipc::Partition_superblock_kind::ext4, 109 | system::disk::SuperblockKind::Luks2 => disks_ipc::Partition_superblock_kind::luks2, 110 | system::disk::SuperblockKind::F2FS => disks_ipc::Partition_superblock_kind::f2fs, 111 | system::disk::SuperblockKind::Xfs => disks_ipc::Partition_superblock_kind::xfs, 112 | // FIXME: introduce FAT kind? 113 | system::disk::SuperblockKind::Fat => disks_ipc::Partition_superblock_kind::unknown, 114 | } 115 | } else { 116 | disks_ipc::Partition_superblock_kind::unknown 117 | }, 118 | }) 119 | .collect::>(); 120 | 121 | // Cache the partitions 122 | cell.insert(disk.path.to_string_lossy().to_string(), res.clone()); 123 | call.reply(res)?; 124 | } 125 | Err(e) => return call.reply_disk_error(e.to_string()), 126 | }, 127 | Err(e) => return call.reply_disk_error(e.to_string()), 128 | } 129 | 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/installer/src/steps/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Lichen installation steps 6 | 7 | use std::{fmt::Debug, process::ExitStatus}; 8 | use thiserror::Error; 9 | 10 | mod context; 11 | pub use context::Context; 12 | 13 | #[derive(Debug, Error)] 14 | pub enum Error { 15 | #[error("io: {0}")] 16 | IO(#[from] std::io::Error), 17 | 18 | #[error("unknown filesystem")] 19 | UnknownFilesystem, 20 | 21 | #[error("no mountpoint given")] 22 | NoMountpoint, 23 | 24 | #[error("command `{program}` exited with {status}")] 25 | CommandFailed { program: String, status: ExitStatus }, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub enum Step<'a> { 30 | AddRepo(Box), 31 | Bind(Box), 32 | CreateUser(Box>), 33 | Format(Box>), 34 | Install(Box), 35 | Mount(Box>), 36 | SetPassword(Box>), 37 | SetLocale(Box>), 38 | SetMachineID(Box), 39 | SetTimezone(Box>), 40 | WriteFstab(Box), 41 | } 42 | 43 | impl<'a> Step<'a> { 44 | /// Create new repo step 45 | pub fn add_repo(r: AddRepo) -> Self { 46 | Self::AddRepo(Box::new(r)) 47 | } 48 | 49 | pub fn create_user(u: CreateAccount<'a>) -> Self { 50 | Self::CreateUser(Box::new(u)) 51 | } 52 | 53 | pub fn install_packages(p: InstallPackages) -> Self { 54 | Self::Install(Box::new(p)) 55 | } 56 | 57 | /// Create new FormatPartition step 58 | pub fn format(f: FormatPartition<'a>) -> Self { 59 | Self::Format(Box::new(f)) 60 | } 61 | 62 | /// Create new MountPartition step 63 | pub fn mount(m: MountPartition<'a>) -> Self { 64 | Self::Mount(Box::new(m)) 65 | } 66 | 67 | /// Create new bind mount 68 | pub fn bind_mount(b: BindMount) -> Self { 69 | Self::Bind(Box::new(b)) 70 | } 71 | 72 | /// Set system locale 73 | pub fn set_locale(l: SetLocale<'a>) -> Self { 74 | Self::SetLocale(Box::new(l)) 75 | } 76 | 77 | /// Set system timezone 78 | pub fn set_timezone(t: SetTimezone<'a>) -> Self { 79 | Self::SetTimezone(Box::new(t)) 80 | } 81 | 82 | /// Set an account password 83 | pub fn set_password(a: SetPassword<'a>) -> Self { 84 | Self::SetPassword(Box::new(a)) 85 | } 86 | 87 | /// Construct a dbus/systemd machine id 88 | pub fn set_machine_id() -> Self { 89 | Self::SetMachineID(Box::new(SetMachineID {})) 90 | } 91 | 92 | // Emit the given fstab 93 | pub fn emit_fstab(f: EmitFstab) -> Self { 94 | Self::WriteFstab(Box::new(f)) 95 | } 96 | 97 | /// Return a unique short ID name for the steps 98 | pub fn name(&self) -> &'static str { 99 | match &self { 100 | Step::AddRepo(_) => "add-repo", 101 | Step::Bind(_) => "bind-mount", 102 | Step::CreateUser(_) => "create-user", 103 | Step::Format(_) => "format-partition", 104 | Step::Install(_) => "install-packages", 105 | Step::Mount(_) => "mount-partition", 106 | Step::SetPassword(_) => "set-password", 107 | Step::SetLocale(_) => "set-locale", 108 | Step::SetTimezone(_) => "set-timezone", 109 | Step::SetMachineID(_) => "set-machine-id", 110 | Step::WriteFstab(_) => "write-fstab", 111 | } 112 | } 113 | 114 | /// Return the display title for a step 115 | pub fn title(&self) -> String { 116 | match &self { 117 | Step::AddRepo(s) => s.title(), 118 | Step::Bind(s) => s.title(), 119 | Step::CreateUser(s) => s.title(), 120 | Step::Format(s) => s.title(), 121 | Step::Install(s) => s.title(), 122 | Step::Mount(s) => s.title(), 123 | Step::SetPassword(s) => s.title(), 124 | Step::SetLocale(s) => s.title(), 125 | Step::SetTimezone(s) => s.title(), 126 | Step::SetMachineID(s) => s.title(), 127 | Step::WriteFstab(s) => s.title(), 128 | } 129 | } 130 | 131 | /// Describe the action/context for the step 132 | pub fn describe(&self) -> String { 133 | match &self { 134 | Step::AddRepo(s) => s.describe(), 135 | Step::Bind(s) => s.describe(), 136 | Step::CreateUser(s) => s.describe(), 137 | Step::Format(s) => s.describe(), 138 | Step::Install(s) => s.describe(), 139 | Step::Mount(s) => s.describe(), 140 | Step::SetPassword(s) => s.describe(), 141 | Step::SetLocale(s) => s.describe(), 142 | Step::SetTimezone(s) => s.describe(), 143 | Step::SetMachineID(s) => s.describe(), 144 | Step::WriteFstab(s) => s.describe(), 145 | } 146 | } 147 | 148 | /// Execute a step asynchronously. Implementations can opt-in to async. 149 | pub fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 150 | match &self { 151 | Step::AddRepo(s) => Ok(s.execute(context)?), 152 | Step::Bind(s) => Ok(s.execute(context)?), 153 | Step::CreateUser(s) => Ok(s.execute(context)?), 154 | Step::Format(s) => Ok(s.execute(context)?), 155 | Step::Install(s) => Ok(s.execute(context)?), 156 | Step::Mount(s) => Ok(s.execute(context)?), 157 | Step::SetPassword(s) => Ok(s.execute(context)?), 158 | Step::SetLocale(s) => Ok(s.execute(context)?), 159 | Step::SetTimezone(s) => Ok(s.execute(context)?), 160 | Step::SetMachineID(s) => Ok(s.execute(context)?), 161 | Step::WriteFstab(s) => Ok(s.execute(context)?), 162 | } 163 | } 164 | 165 | /// Determine whether an indeterminate progress spinner is needed 166 | /// In the CLI frontend this is abused to hide the progressbar when invoking moss. 167 | pub fn is_indeterminate(&self) -> bool { 168 | !matches!(self, Step::Install(_)) 169 | } 170 | } 171 | 172 | mod partitions; 173 | 174 | pub use partitions::{BindMount, FormatPartition, MountPartition, Unmount}; 175 | 176 | mod packaging; 177 | pub use packaging::{AddRepo, InstallPackages}; 178 | 179 | mod cleanup; 180 | pub use cleanup::Cleanup; 181 | 182 | mod postinstall; 183 | pub use postinstall::{CreateAccount, EmitFstab, FstabEntry, SetLocale, SetMachineID, SetPassword, SetTimezone}; 184 | -------------------------------------------------------------------------------- /crates/system/src/locale/registry.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Registry of languages and territories. 6 | 7 | use std::collections::HashMap; 8 | 9 | use fs_err as fs; 10 | 11 | use super::{iso_3166, iso_639_2, iso_639_3, Error, Language, Locale, Territory}; 12 | 13 | /// All ISO codes are expected to live in this location 14 | const ISO_CODES_BASE: &str = "/usr/share/iso-codes/json"; 15 | 16 | /// Manage locales + territories 17 | pub struct Registry { 18 | places: Vec, 19 | places_lookup: HashMap, 20 | languages: Vec, 21 | languages_lookup: HashMap, 22 | } 23 | 24 | impl Registry { 25 | /// Create a new locale registry from the system iso-code JSON definitions 26 | pub fn new() -> Result { 27 | let places = Self::load_territories()?; 28 | let mut places_lookup = HashMap::new(); 29 | for (index, item) in places.iter().enumerate() { 30 | places_lookup.insert(item.code2.to_lowercase(), index); 31 | places_lookup.insert(item.code.to_lowercase(), index); 32 | } 33 | 34 | // Convert all languages into usable ones with mapping 35 | let mut languages = Self::load_languages_2()?; 36 | languages.extend(Self::load_languages_3()?); 37 | let mut languages_lookup = HashMap::new(); 38 | for (index, language) in languages.iter().enumerate() { 39 | if let Some(code2) = language.code2.as_ref() { 40 | languages_lookup.insert(code2.to_lowercase(), index); 41 | } 42 | languages_lookup.insert(language.code.to_lowercase(), index); 43 | } 44 | 45 | Ok(Self { 46 | places, 47 | places_lookup, 48 | languages, 49 | languages_lookup, 50 | }) 51 | } 52 | 53 | /// Load all the territories 54 | fn load_territories() -> Result, Error> { 55 | // Load the territories in 56 | let territories = format!("{ISO_CODES_BASE}/iso_3166-1.json"); 57 | let contents = fs::read_to_string(territories)?; 58 | let parser = serde_json::from_str::>(&contents)?; 59 | 60 | Ok(parser.entries.iter().map(|e| e.into()).collect::>()) 61 | } 62 | 63 | /// Load the 2 DB 64 | fn load_languages_2() -> Result, Error> { 65 | let languages = format!("{ISO_CODES_BASE}/iso_639-2.json"); 66 | let contents = fs::read_to_string(languages)?; 67 | let parser = serde_json::from_str::>(&contents)?; 68 | 69 | Ok(parser.entries.iter().map(|e| e.into()).collect::>()) 70 | } 71 | 72 | /// Load the 3 DB 73 | fn load_languages_3() -> Result, Error> { 74 | let languages = format!("{ISO_CODES_BASE}/iso_639-3.json"); 75 | let contents = fs::read_to_string(languages)?; 76 | let parser = serde_json::from_str::>(&contents)?; 77 | 78 | Ok(parser.entries.iter().map(|e| e.into()).collect::>()) 79 | } 80 | 81 | /// Retrieve the territory for the given (lower-case) code 82 | pub fn territory(&self, id: impl AsRef) -> Option<&Territory> { 83 | if let Some(idx) = self.places_lookup.get(id.as_ref()) { 84 | self.places.get(*idx) 85 | } else { 86 | None 87 | } 88 | } 89 | 90 | /// Retrieve the language for the given (lower-case) code 91 | pub fn language(&self, id: impl AsRef) -> Option<&Language> { 92 | if let Some(idx) = self.languages_lookup.get(id.as_ref()) { 93 | self.languages.get(*idx) 94 | } else { 95 | None 96 | } 97 | } 98 | 99 | /// Attempt to retrieve a locale combination 100 | pub fn locale(&self, id: impl AsRef) -> Option> { 101 | let id = id.as_ref().to_lowercase(); 102 | 103 | // Handle .codeset 104 | let (left, codeset) = if let Some(idx) = id.find('.') { 105 | id.split_at(idx) 106 | } else { 107 | (id.as_str(), "") 108 | }; 109 | 110 | // Fix "utf8" codeset 111 | let codeset = if codeset.is_empty() { 112 | None 113 | } else { 114 | Some( 115 | codeset 116 | .replace("utf8", "UTF-8") 117 | .chars() 118 | .skip(1) 119 | .collect::() 120 | .to_uppercase(), 121 | ) 122 | }; 123 | 124 | // Now handle a modifier 125 | let (code, modifier) = if let Some(idx) = left.find('@') { 126 | left.split_at(idx) 127 | } else { 128 | (left, "") 129 | }; 130 | let modifier = if modifier.is_empty() { 131 | None 132 | } else { 133 | Some(modifier.chars().skip(1).collect::().to_uppercase()) 134 | }; 135 | 136 | // Split on '_' and map into language/territory 137 | let (l_code, t_code) = code.split_once('_')?; 138 | let language = self.language(l_code)?; 139 | let territory = self.territory(t_code)?; 140 | 141 | // Cook functioning names/ids with fixed formatting 142 | let display_name = format!("{} ({})", &language.display_name, &territory.display_name); 143 | let mut new_id = Vec::new(); 144 | new_id.push(l_code.into()); 145 | new_id.push("_".into()); 146 | new_id.push(t_code.to_uppercase()); 147 | if let Some(m) = modifier.as_ref() { 148 | new_id.push(format!("@{m}")); 149 | } 150 | if let Some(codeset) = codeset.as_ref() { 151 | new_id.push(format!(".{codeset}")); 152 | } 153 | 154 | Some(Locale { 155 | name: new_id.into_iter().collect(), 156 | display_name, 157 | language, 158 | territory, 159 | codeset, 160 | modifier, 161 | }) 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use std::process::Command; 168 | 169 | use super::Registry; 170 | 171 | #[test] 172 | fn test_territory() { 173 | let r = Registry::new().expect("Failed to initialise registry"); 174 | let ie = r.territory("ie").expect("Cannot find Ireland by ie"); 175 | let irl = r.territory("irl").expect("Cannot find Ireland by irl"); 176 | assert_eq!(ie, irl); 177 | assert_eq!(irl.display_name, "Ireland"); 178 | 179 | let dk = r.territory("dk").expect("Cannot find Denmark by dk"); 180 | assert_eq!(dk.display_name, "Kingdom of Denmark"); 181 | eprintln!("dk = {dk:?}"); 182 | } 183 | 184 | #[test] 185 | fn test_language() { 186 | let r = Registry::new().expect("Failed to initialise registry"); 187 | let en = r.language("en").expect("Cannot find English by en"); 188 | assert_eq!(en.display_name, "English"); 189 | 190 | let dan = r.language("dan").expect("Cannot find Danish by dan"); 191 | let dn = r.language("da").expect("Cannot find Danish by dn"); 192 | assert_eq!(dan, dn); 193 | } 194 | 195 | #[test] 196 | fn test_locale() { 197 | let r = Registry::new().expect("Failed to initialise registry"); 198 | let en_ie = r.locale("en_IE.UTF-8").expect("Failed to find en_IE.UTF-8"); 199 | assert_eq!(en_ie.display_name, "English (Ireland)"); 200 | let ga_ie = r.locale("ga_IE.UTF-8").expect("Failed to find ga_IE.UTF-8"); 201 | assert_eq!(ga_ie.display_name, "Irish (Ireland)"); 202 | 203 | eprintln!("en_IE = {en_ie:?}"); 204 | eprintln!("ga_IE = {ga_ie:?}"); 205 | } 206 | 207 | #[test] 208 | fn test_get_locales() { 209 | let r = Registry::new().expect("Failed to initialise registry"); 210 | let output = Command::new("localectl") 211 | .arg("list-locales") 212 | .output() 213 | .expect("Failed to run localectl"); 214 | let output = String::from_utf8(output.stdout).expect("Cannot decode output"); 215 | for line in output.lines() { 216 | if line == "C.UTF-8" { 217 | continue; 218 | } 219 | eprintln!("looking up {line}"); 220 | let locale = r 221 | .locale(line) 222 | .unwrap_or_else(|| panic!("Failed to find a predefined locale {line}")); 223 | eprintln!("locale {line} = {locale:?}"); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /crates/installer/src/steps/postinstall.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Post-installation tasks 6 | 7 | use std::{fmt::Display, process::Command}; 8 | 9 | use fs_err as fs; 10 | use system::locale::Locale; 11 | 12 | use crate::{Account, SystemPartition}; 13 | 14 | use super::{Context, Error}; 15 | 16 | /// Configure an account on the system 17 | #[derive(Debug)] 18 | pub struct SetPassword<'a> { 19 | pub(crate) account: &'a Account, 20 | pub(crate) password: String, 21 | } 22 | 23 | /// Create an account 24 | #[derive(Debug)] 25 | pub struct CreateAccount<'a> { 26 | pub(crate) account: &'a Account, 27 | } 28 | 29 | impl<'a> SetPassword<'a> { 30 | pub(super) fn title(&self) -> String { 31 | "Set account password".to_string() 32 | } 33 | 34 | pub(super) fn describe(&self) -> String { 35 | self.account.username.clone() 36 | } 37 | 38 | /// Execute to configure the account 39 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 40 | let mut cmd = Command::new("chroot"); 41 | cmd.arg(context.root().clone()); 42 | cmd.arg("chpasswd"); 43 | 44 | let password_text = format!("{}:{}\n", &self.account.username, self.password); 45 | context.run_command_captured(&mut cmd, Some(&password_text))?; 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl<'a> CreateAccount<'a> { 52 | pub(super) fn title(&self) -> String { 53 | "Create account".to_string() 54 | } 55 | 56 | pub(super) fn describe(&self) -> String { 57 | self.account.username.clone() 58 | } 59 | 60 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 61 | let mut cmd = Command::new("chroot"); 62 | cmd.arg(context.root().clone()); 63 | cmd.arg("useradd"); 64 | cmd.arg(self.account.username.clone()); 65 | cmd.args(["-m", "-U", "-G", "audio,adm,wheel,render,kvm,input,users"]); 66 | 67 | if let Some(gecos) = self.account.gecos.as_ref() { 68 | cmd.arg("-C"); 69 | cmd.arg(gecos.clone()); 70 | } 71 | cmd.arg("-s"); 72 | cmd.arg(self.account.shell.clone()); 73 | context.run_command_captured(&mut cmd, None)?; 74 | Ok(()) 75 | } 76 | } 77 | 78 | /// Update locale in `locale.conf` 79 | #[derive(Debug)] 80 | pub struct SetLocale<'a> { 81 | pub(crate) locale: &'a Locale<'a>, 82 | } 83 | 84 | impl<'a> SetLocale<'a> { 85 | pub(super) fn title(&self) -> String { 86 | "Set system locale".to_string() 87 | } 88 | 89 | pub(super) fn describe(&self) -> String { 90 | self.locale.display_name.clone() 91 | } 92 | 93 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 94 | let contents = format!("LANG={}\n", self.locale.name); 95 | let path = context.root().join("etc").join("locale.conf"); 96 | fs::write(path, &contents)?; 97 | 98 | Ok(()) 99 | } 100 | } 101 | 102 | // Update the timezone 103 | #[derive(Debug)] 104 | pub struct SetTimezone<'a> { 105 | pub(crate) timezone: &'a str, 106 | } 107 | 108 | impl<'a> SetTimezone<'a> { 109 | pub(super) fn title(&self) -> String { 110 | "Set system timezone".to_string() 111 | } 112 | 113 | pub(super) fn describe(&self) -> String { 114 | self.timezone.to_string() 115 | } 116 | 117 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 118 | fs::remove_file(context.root().join("etc").join("localtime")).ok(); 119 | std::os::unix::fs::symlink( 120 | format!("../usr/share/zoneinfo/{}", self.timezone), 121 | context.root().join("etc").join("localtime"), 122 | )?; 123 | 124 | Ok(()) 125 | } 126 | } 127 | 128 | /// Set a machine ID up in the root 129 | #[derive(Debug)] 130 | pub struct SetMachineID {} 131 | 132 | impl<'a> SetMachineID { 133 | pub(super) fn title(&self) -> String { 134 | "Allocate machine-id".to_string() 135 | } 136 | 137 | pub(super) fn describe(&self) -> String { 138 | "via systemd-machine-id-setup".to_string() 139 | } 140 | 141 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 142 | let file = context.root().join("etc").join("machine-id"); 143 | if file.exists() { 144 | fs::remove_file(file)?; 145 | } 146 | 147 | let mut cmd = Command::new("chroot"); 148 | cmd.arg(context.root().clone()); 149 | cmd.arg("systemd-machine-id-setup"); 150 | context.run_command_captured(&mut cmd, None)?; 151 | 152 | Ok(()) 153 | } 154 | } 155 | 156 | #[derive(Debug)] 157 | pub enum FstabEntry { 158 | Comment(String), 159 | Device { 160 | fs: String, 161 | mountpoint: String, 162 | kind: String, 163 | opts: String, 164 | dump: u8, 165 | pass: u8, 166 | }, 167 | } 168 | 169 | impl Display for FstabEntry { 170 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 171 | match self { 172 | FstabEntry::Comment(c) => f.write_fmt(format_args!("# {c}")), 173 | FstabEntry::Device { 174 | fs, 175 | mountpoint, 176 | kind, 177 | opts, 178 | dump, 179 | pass, 180 | } => f.write_fmt(format_args!("{fs}\t{mountpoint}\t{kind}\t{opts}\t{dump}\t{pass}")), 181 | } 182 | } 183 | } 184 | 185 | // Emit the fstab 186 | #[derive(Debug)] 187 | pub struct EmitFstab { 188 | entries: Vec, 189 | } 190 | 191 | impl Default for EmitFstab { 192 | fn default() -> Self { 193 | Self { 194 | entries: vec![ 195 | // template header 196 | FstabEntry::Comment("/etc/fstab: static filesystem information.".to_string()), 197 | FstabEntry::Comment(String::new()), 198 | FstabEntry::Comment(" ".to_string()), 199 | FstabEntry::Comment(String::new()), 200 | FstabEntry::Comment("/dev/ROOT / ext3 noatime 0 1".to_string()), 201 | FstabEntry::Comment("/dev/SWAP none swap sw 0 0".to_string()), 202 | FstabEntry::Comment("/dev/fd0 /mnt/floppy auto noauto 0 0".to_string()), 203 | // proc 204 | FstabEntry::Device { 205 | fs: "none".into(), 206 | mountpoint: "/proc".into(), 207 | kind: "proc".into(), 208 | opts: "nosuid,noexec".into(), 209 | dump: 0, 210 | pass: 0, 211 | }, 212 | // shm 213 | FstabEntry::Device { 214 | fs: "none".into(), 215 | mountpoint: "/dev/shm".into(), 216 | kind: "tmpfs".into(), 217 | opts: "defaults".into(), 218 | dump: 0, 219 | pass: 0, 220 | }, 221 | ], 222 | } 223 | } 224 | } 225 | 226 | impl TryFrom<&SystemPartition> for FstabEntry { 227 | type Error = Error; 228 | fn try_from(value: &SystemPartition) -> Result { 229 | // Honestly, this is a bit ext4 centric, no ssd care given 230 | let s = Self::Device { 231 | // NOTE: This is always PartUUID for us, we only do GPT. 232 | fs: format!("PARTUUID={}", &value.partition.uuid), 233 | mountpoint: value.mountpoint.clone().ok_or(Error::NoMountpoint)?, 234 | kind: value 235 | .partition 236 | .sb 237 | .as_ref() 238 | .map(|sb| sb.to_string()) 239 | .ok_or(Error::UnknownFilesystem)?, 240 | opts: "rw,errors=remount-ro".to_string(), 241 | dump: 0, 242 | pass: 1, 243 | }; 244 | 245 | Ok(s) 246 | } 247 | } 248 | 249 | impl<'a> EmitFstab { 250 | // Create with a bunch of entries 251 | pub fn with_entries(self, entries: impl IntoIterator) -> Self { 252 | Self { 253 | entries: self.entries.into_iter().chain(entries).collect::>(), 254 | } 255 | } 256 | 257 | pub(super) fn title(&self) -> String { 258 | "Generate fstab".into() 259 | } 260 | 261 | pub(super) fn describe(&self) -> String { 262 | "".into() 263 | } 264 | 265 | /// Write the filesystem table 266 | pub(super) fn execute(&self, context: &'a impl Context<'a>) -> Result<(), Error> { 267 | let file = context.root().join("etc").join("fstab"); 268 | let entries = self.entries.iter().map(|e| e.to_string()).collect::>(); 269 | fs::write(file, entries.join("\n"))?; 270 | Ok(()) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /crates/installer/src/engine.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Concrete implementation of the installer 6 | 7 | use std::path::Path; 8 | 9 | use system::{ 10 | disk::{self, Disk}, 11 | locale::{self, Locale}, 12 | }; 13 | use thiserror::Error; 14 | use topology::disk::Builder; 15 | 16 | use crate::{ 17 | steps::{ 18 | self, AddRepo, BindMount, Cleanup, Context, CreateAccount, EmitFstab, FormatPartition, FstabEntry, 19 | InstallPackages, MountPartition, SetLocale, SetPassword, Step, Unmount, 20 | }, 21 | BootPartition, Model, SystemPartition, 22 | }; 23 | 24 | #[derive(Debug, Error)] 25 | pub enum Error { 26 | #[error("disk: {0}")] 27 | Disk(#[from] disk::Error), 28 | 29 | #[error("locale: {0}")] 30 | Locale(#[from] locale::Error), 31 | 32 | #[error("missing mandatory partition: {0}")] 33 | MissingPartition(&'static str), 34 | 35 | #[error("steps: {0}")] 36 | Steps(#[from] steps::Error), 37 | 38 | #[error("unknown locale code: {0}")] 39 | UnknownLocale(String), 40 | 41 | #[error("topology: {0}")] 42 | Topology(#[from] topology::disk::Error), 43 | } 44 | 45 | /// The installer does some initial probing and is used with a Model 46 | /// to build an execution routine 47 | pub struct Installer { 48 | /// Complete locale registry 49 | locale_registry: locale::Registry, 50 | 51 | /// Boot partitions 52 | boot_parts: Vec, 53 | 54 | /// System partitions 55 | system_parts: Vec, 56 | } 57 | 58 | impl Installer { 59 | /// Return a newly initialised installer 60 | pub fn new() -> Result { 61 | let locale_registry = locale::Registry::new()?; 62 | let disks = Disk::discover()?; 63 | 64 | // Figure out where we live right now and exclude the rootfs 65 | let probe = Builder::default().build()?; 66 | let root_nodes = match probe.get_rootfs_device("/") { 67 | Ok(device) => { 68 | let mut nodes = probe.get_device_chain(&device.path).unwrap_or_default(); 69 | nodes.push(device.path.into()); 70 | nodes 71 | } 72 | _ => { 73 | vec![] 74 | } 75 | }; 76 | 77 | // Exclude parent block devices related to `/` partition 78 | let parents = root_nodes 79 | .iter() 80 | .filter_map(|n| probe.get_device_parent(n)) 81 | .collect::>(); 82 | 83 | let mut boot_parts = vec![]; 84 | let mut system_parts = vec![]; 85 | for disk in disks.iter().filter(|d| !parents.contains(&d.path)) { 86 | let parts = match disk.partitions() { 87 | Ok(parts) => parts, 88 | Err(e) => { 89 | log::trace!("Failed to get partitions for `{disk}`: {e}"); 90 | continue; 91 | } 92 | }; 93 | 94 | // Exclude partitions related to `/` partition 95 | let parts = parts 96 | .into_iter() 97 | .filter(|p| !root_nodes.contains(&p.path)) 98 | .collect::>(); 99 | if let Some(esp) = parts 100 | .iter() 101 | .find(|p| matches!(p.kind, disk::PartitionKind::ESP)) 102 | .cloned() 103 | { 104 | let xbootldr = parts 105 | .iter() 106 | .find(|p| matches!(p.kind, disk::PartitionKind::XBOOTLDR)) 107 | .cloned(); 108 | boot_parts.push(BootPartition { 109 | esp, 110 | xbootldr, 111 | parent_desc: disk.to_string(), 112 | }); 113 | } 114 | let others = parts 115 | .iter() 116 | .filter(|p| matches!(p.kind, disk::PartitionKind::Regular)) 117 | .cloned(); 118 | system_parts.extend(others.map(|p| SystemPartition { 119 | partition: p, 120 | mountpoint: None, 121 | parent_desc: disk.to_string(), 122 | })); 123 | } 124 | 125 | Ok(Self { 126 | locale_registry, 127 | system_parts, 128 | boot_parts, 129 | }) 130 | } 131 | 132 | /// Allow access to locale registry (mapping IDs) 133 | pub fn locales(&self) -> &locale::Registry { 134 | &self.locale_registry 135 | } 136 | 137 | /// Generate/load the locale map 138 | pub fn locales_for_ids>>(&self, ids: S) -> Result>, Error> { 139 | let res = ids 140 | .into_iter() 141 | .filter_map(|id| self.locale_registry.locale(id)) 142 | .collect::>(); 143 | 144 | Ok(res) 145 | } 146 | 147 | /// Return references to the discovered boot partitions 148 | pub fn boot_partitions(&self) -> &[BootPartition] { 149 | &self.boot_parts 150 | } 151 | 152 | /// Return references to the discovered system partitions 153 | pub fn system_partitions(&self) -> &[SystemPartition] { 154 | &self.system_parts 155 | } 156 | 157 | /// build the model into a set of install steps 158 | pub fn compile_to_steps<'a>( 159 | &'a self, 160 | model: &'a Model<'_>, 161 | context: &'a impl Context<'a>, 162 | ) -> Result<(Vec, Vec>), Error> { 163 | let mut s: Vec> = vec![]; 164 | let mut c: Vec = vec![]; 165 | let boot_part = &model.boot_partition.esp; 166 | 167 | let root_partition = model 168 | .partitions 169 | .iter() 170 | .find(|p| { 171 | if let Some(mount) = p.mountpoint.as_ref() { 172 | mount == "/" 173 | } else { 174 | false 175 | } 176 | }) 177 | .ok_or(Error::MissingPartition("/"))?; 178 | 179 | // Must format and mount `/` before we can add more mounts 180 | s.push(Step::format(FormatPartition { 181 | partition: &root_partition.partition, 182 | filesystem: model.rootfs_type.clone(), 183 | })); 184 | s.push(Step::mount(MountPartition { 185 | partition: &root_partition.partition, 186 | mountpoint: context.root().clone(), 187 | })); 188 | c.push(Cleanup::unmount(Unmount { 189 | mountpoint: context.root().clone(), 190 | })); 191 | 192 | // Mount the ESP 193 | s.push(Step::mount(MountPartition { 194 | partition: boot_part, 195 | mountpoint: context.root().join("efi"), 196 | })); 197 | c.push(Cleanup::unmount(Unmount { 198 | mountpoint: context.root().join("efi"), 199 | })); 200 | 201 | // Mount xbootldr at `/boot` if present 202 | if let Some(xbootldr) = model.boot_partition.xbootldr.as_ref() { 203 | s.push(Step::mount(MountPartition { 204 | partition: xbootldr, 205 | mountpoint: context.root().join("boot"), 206 | })); 207 | c.push(Cleanup::unmount(Unmount { 208 | mountpoint: context.root().join("boot"), 209 | })); 210 | }; 211 | 212 | // Populate vfs bind mounts 213 | let (mounts, unmounts) = self.create_vfs_mounts(context.root()); 214 | s.extend(mounts); 215 | c.extend(unmounts); 216 | 217 | // HAX: 218 | s.push(Step::add_repo(AddRepo { 219 | uri: "https://cdn.aerynos.dev/unstable/x86_64/stone.index".into(), 220 | name: "unstable".into(), 221 | priority: 0, 222 | })); 223 | s.push(Step::install_packages(InstallPackages { 224 | names: model.packages.iter().cloned().collect::>(), 225 | })); 226 | 227 | // Update any passwords 228 | for account in model.accounts.iter() { 229 | if !account.builtin { 230 | s.push(Step::create_user(CreateAccount { account })); 231 | } 232 | if let Some(password) = account.password.clone() { 233 | s.push(Step::set_password(SetPassword { account, password })); 234 | } 235 | } 236 | 237 | // System locale 238 | if let Some(locale) = model.locale { 239 | s.push(Step::set_locale(SetLocale { locale })); 240 | } 241 | 242 | // System timezone 243 | if let Some(timezone) = model.timezone.as_ref() { 244 | s.push(Step::set_timezone(steps::SetTimezone { timezone })); 245 | } 246 | 247 | // Ensure we get a machine-id.. 248 | s.push(Step::set_machine_id()); 249 | 250 | // Write the fstab 251 | let fstab = EmitFstab::default().with_entries([ 252 | FstabEntry::Comment(format!( 253 | "{} at time of installation", 254 | root_partition.partition.path.display() 255 | )), 256 | FstabEntry::try_from(root_partition)?, 257 | ]); 258 | s.push(Step::emit_fstab(fstab)); 259 | 260 | // Get the sync call in for unmounts 261 | c.push(Cleanup::sync_fs()); 262 | // Lastly, flip cleanups to front in reverse (due to mounts) 263 | c.reverse(); 264 | Ok((c, s)) 265 | } 266 | 267 | fn create_vfs_mounts(&self, prefix: &Path) -> (Vec>, Vec) { 268 | const PARTS: &[(&str, &str); 5] = &[ 269 | ("/dev", "dev"), 270 | ("/dev/shm", "dev/shm"), 271 | ("/dev/pts", "dev/pts"), 272 | ("/proc", "proc"), 273 | ("/sys", "sys"), 274 | ]; 275 | PARTS 276 | .iter() 277 | .map(|(source, dest)| { 278 | ( 279 | Step::bind_mount(BindMount { 280 | source: source.into(), 281 | dest: prefix.join(dest), 282 | }), 283 | Cleanup::unmount(Unmount { 284 | mountpoint: prefix.join(dest), 285 | }), 286 | ) 287 | }) 288 | .collect::<(Vec<_>, Vec<_>)>() 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /lichen_ipc/src/com_serpentos_lichen_disks.rs: -------------------------------------------------------------------------------- 1 | #![doc = "This file was automatically generated by the varlink rust generator"] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | use serde_derive::{Deserialize, Serialize}; 5 | use std::io::BufRead; 6 | use std::sync::{Arc, RwLock}; 7 | use varlink::{self, CallTrait}; 8 | #[allow(dead_code)] 9 | #[derive(Clone, PartialEq, Debug)] 10 | #[allow(clippy::enum_variant_names)] 11 | pub enum ErrorKind { 12 | Varlink_Error, 13 | VarlinkReply_Error, 14 | DiskError(Option), 15 | } 16 | impl ::std::fmt::Display for ErrorKind { 17 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 18 | match self { 19 | ErrorKind::Varlink_Error => write!(f, "Varlink Error"), 20 | ErrorKind::VarlinkReply_Error => write!(f, "Varlink error reply"), 21 | ErrorKind::DiskError(v) => write!(f, "com.serpentos.lichen.disks.DiskError: {:#?}", v), 22 | } 23 | } 24 | } 25 | pub struct Error( 26 | pub ErrorKind, 27 | pub Option>, 28 | pub Option<&'static str>, 29 | ); 30 | impl Error { 31 | #[allow(dead_code)] 32 | pub fn kind(&self) -> &ErrorKind { 33 | &self.0 34 | } 35 | } 36 | impl From for Error { 37 | fn from(e: ErrorKind) -> Self { 38 | Error(e, None, None) 39 | } 40 | } 41 | impl std::error::Error for Error { 42 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 43 | self.1 44 | .as_ref() 45 | .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)) 46 | } 47 | } 48 | impl std::fmt::Display for Error { 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 50 | std::fmt::Display::fmt(&self.0, f) 51 | } 52 | } 53 | impl std::fmt::Debug for Error { 54 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 55 | use std::error::Error as StdError; 56 | if let Some(ref o) = self.2 { 57 | std::fmt::Display::fmt(o, f)?; 58 | } 59 | std::fmt::Debug::fmt(&self.0, f)?; 60 | if let Some(e) = self.source() { 61 | std::fmt::Display::fmt("\nCaused by:\n", f)?; 62 | std::fmt::Debug::fmt(&e, f)?; 63 | } 64 | Ok(()) 65 | } 66 | } 67 | #[allow(dead_code)] 68 | pub type Result = std::result::Result; 69 | impl From for Error { 70 | fn from(e: varlink::Error) -> Self { 71 | match e.kind() { 72 | varlink::ErrorKind::VarlinkErrorReply(r) => Error( 73 | ErrorKind::from(r), 74 | Some(Box::from(e)), 75 | Some(concat!(file!(), ":", line!(), ": ")), 76 | ), 77 | _ => Error( 78 | ErrorKind::Varlink_Error, 79 | Some(Box::from(e)), 80 | Some(concat!(file!(), ":", line!(), ": ")), 81 | ), 82 | } 83 | } 84 | } 85 | #[allow(dead_code)] 86 | impl Error { 87 | pub fn source_varlink_kind(&self) -> Option<&varlink::ErrorKind> { 88 | use std::error::Error as StdError; 89 | let mut s: &dyn StdError = self; 90 | while let Some(c) = s.source() { 91 | let k = self 92 | .source() 93 | .and_then(|e| e.downcast_ref::()) 94 | .map(|e| e.kind()); 95 | if k.is_some() { 96 | return k; 97 | } 98 | s = c; 99 | } 100 | None 101 | } 102 | } 103 | impl From<&varlink::Reply> for ErrorKind { 104 | #[allow(unused_variables)] 105 | fn from(e: &varlink::Reply) -> Self { 106 | match e { 107 | varlink::Reply { error: Some(ref t), .. } if t == "com.serpentos.lichen.disks.DiskError" => match e { 108 | varlink::Reply { 109 | parameters: Some(p), .. 110 | } => match serde_json::from_value(p.clone()) { 111 | Ok(v) => ErrorKind::DiskError(v), 112 | Err(_) => ErrorKind::DiskError(None), 113 | }, 114 | _ => ErrorKind::DiskError(None), 115 | }, 116 | _ => ErrorKind::VarlinkReply_Error, 117 | } 118 | } 119 | } 120 | pub trait VarlinkCallError: varlink::CallTrait { 121 | fn reply_disk_error(&mut self, r#message: String) -> varlink::Result<()> { 122 | self.reply_struct(varlink::Reply::error( 123 | "com.serpentos.lichen.disks.DiskError", 124 | Some(serde_json::to_value(DiskError_Args { r#message }).map_err(varlink::map_context!())?), 125 | )) 126 | } 127 | } 128 | impl<'a> VarlinkCallError for varlink::Call<'a> {} 129 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 130 | pub enum r#Disk_kind { 131 | r#ssd, 132 | r#hdd, 133 | } 134 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 135 | pub struct r#Disk { 136 | pub r#kind: Disk_kind, 137 | pub r#path: String, 138 | pub r#model: Option, 139 | pub r#vendor: Option, 140 | pub r#size: i64, 141 | pub r#block_size: i64, 142 | } 143 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 144 | pub enum r#Partition_kind { 145 | r#esp, 146 | r#xbootldr, 147 | r#regular, 148 | } 149 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 150 | pub enum r#Partition_superblock_kind { 151 | r#btrfs, 152 | r#ext4, 153 | r#f2fs, 154 | r#luks2, 155 | r#xfs, 156 | r#unknown, 157 | } 158 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 159 | pub struct r#Partition { 160 | pub r#path: String, 161 | pub r#kind: Partition_kind, 162 | pub r#size: i64, 163 | pub r#uuid: String, 164 | pub r#superblock_kind: Partition_superblock_kind, 165 | } 166 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 167 | pub struct DiskError_Args { 168 | pub r#message: String, 169 | } 170 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 171 | pub struct GetDisks_Reply { 172 | pub r#disks: Vec, 173 | } 174 | impl varlink::VarlinkReply for GetDisks_Reply {} 175 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 176 | pub struct GetDisks_Args {} 177 | pub trait Call_GetDisks: VarlinkCallError { 178 | fn reply(&mut self, r#disks: Vec) -> varlink::Result<()> { 179 | self.reply_struct(GetDisks_Reply { r#disks }.into()) 180 | } 181 | } 182 | impl<'a> Call_GetDisks for varlink::Call<'a> {} 183 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 184 | pub struct GetPartitions_Reply { 185 | pub r#partitions: Vec, 186 | } 187 | impl varlink::VarlinkReply for GetPartitions_Reply {} 188 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 189 | pub struct GetPartitions_Args { 190 | pub r#disk: String, 191 | } 192 | pub trait Call_GetPartitions: VarlinkCallError { 193 | fn reply(&mut self, r#partitions: Vec) -> varlink::Result<()> { 194 | self.reply_struct(GetPartitions_Reply { r#partitions }.into()) 195 | } 196 | } 197 | impl<'a> Call_GetPartitions for varlink::Call<'a> {} 198 | pub trait VarlinkInterface { 199 | fn get_disks(&self, call: &mut dyn Call_GetDisks) -> varlink::Result<()>; 200 | fn get_partitions(&self, call: &mut dyn Call_GetPartitions, r#disk: String) -> varlink::Result<()>; 201 | fn call_upgraded(&self, _call: &mut varlink::Call, _bufreader: &mut dyn BufRead) -> varlink::Result> { 202 | Ok(Vec::new()) 203 | } 204 | } 205 | pub trait VarlinkClientInterface { 206 | fn get_disks(&mut self) -> varlink::MethodCall; 207 | fn get_partitions(&mut self, r#disk: String) 208 | -> varlink::MethodCall; 209 | } 210 | #[allow(dead_code)] 211 | pub struct VarlinkClient { 212 | connection: Arc>, 213 | } 214 | impl VarlinkClient { 215 | #[allow(dead_code)] 216 | pub fn new(connection: Arc>) -> Self { 217 | VarlinkClient { connection } 218 | } 219 | } 220 | impl VarlinkClientInterface for VarlinkClient { 221 | fn get_disks(&mut self) -> varlink::MethodCall { 222 | varlink::MethodCall::::new( 223 | self.connection.clone(), 224 | "com.serpentos.lichen.disks.GetDisks", 225 | GetDisks_Args {}, 226 | ) 227 | } 228 | fn get_partitions( 229 | &mut self, 230 | r#disk: String, 231 | ) -> varlink::MethodCall { 232 | varlink::MethodCall::::new( 233 | self.connection.clone(), 234 | "com.serpentos.lichen.disks.GetPartitions", 235 | GetPartitions_Args { r#disk }, 236 | ) 237 | } 238 | } 239 | #[allow(dead_code)] 240 | pub struct VarlinkInterfaceProxy { 241 | inner: Box, 242 | } 243 | #[allow(dead_code)] 244 | pub fn new(inner: Box) -> VarlinkInterfaceProxy { 245 | VarlinkInterfaceProxy { inner } 246 | } 247 | impl varlink::Interface for VarlinkInterfaceProxy { 248 | fn get_description(&self) -> &'static str { 249 | "# Disk enumeration APIs for Lichen\ninterface com.serpentos.lichen.disks\n\ntype Disk(\n kind: (ssd, hdd),\n path: string,\n model: ?string,\n vendor: ?string,\n size: int,\n block_size: int\n)\n\ntype Partition(\n path: string,\n kind: (esp, xbootldr, regular),\n size: int,\n uuid: string,\n superblock_kind: (btrfs, ext4, f2fs, luks2, xfs, unknown)\n)\n\nerror DiskError(\n message: string\n)\n\n# Enumerate all known disks\nmethod GetDisks() -> (disks:[]Disk)\nmethod GetPartitions(disk: string) -> (partitions:[]Partition)\n" 250 | } 251 | fn get_name(&self) -> &'static str { 252 | "com.serpentos.lichen.disks" 253 | } 254 | fn call_upgraded(&self, call: &mut varlink::Call, bufreader: &mut dyn BufRead) -> varlink::Result> { 255 | self.inner.call_upgraded(call, bufreader) 256 | } 257 | fn call(&self, call: &mut varlink::Call) -> varlink::Result<()> { 258 | let req = call.request.unwrap(); 259 | match req.method.as_ref() { 260 | "com.serpentos.lichen.disks.GetDisks" => self.inner.get_disks(call as &mut dyn Call_GetDisks), 261 | "com.serpentos.lichen.disks.GetPartitions" => { 262 | if let Some(args) = req.parameters.clone() { 263 | let args: GetPartitions_Args = match serde_json::from_value(args) { 264 | Ok(v) => v, 265 | Err(e) => { 266 | let es = format!("{}", e); 267 | let _ = call.reply_invalid_parameter(es.clone()); 268 | return Err(varlink::context!(varlink::ErrorKind::SerdeJsonDe(es))); 269 | } 270 | }; 271 | self.inner 272 | .get_partitions(call as &mut dyn Call_GetPartitions, args.r#disk) 273 | } else { 274 | call.reply_invalid_parameter("parameters".into()) 275 | } 276 | } 277 | m => call.reply_method_not_found(String::from(m)), 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lichen_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | //! Super basic CLI runner for lichen 6 | 7 | use std::{ 8 | io::Write, 9 | path::PathBuf, 10 | process::{Command, Output, Stdio}, 11 | str::FromStr, 12 | time::Duration, 13 | }; 14 | 15 | use color_eyre::eyre::ensure; 16 | use console::{set_colors_enabled, style}; 17 | use crossterm::style::Stylize; 18 | use indicatif::ProgressStyle; 19 | use indoc::indoc; 20 | use installer::{ 21 | selections::{self, Group}, 22 | steps::Context, 23 | systemd, Account, BootPartition, Installer, Locale, SystemPartition, 24 | }; 25 | use nix::libc::geteuid; 26 | 27 | include!(concat!(env!("OUT_DIR"), "/selections.rs")); 28 | 29 | #[derive(Debug)] 30 | struct CliContext { 31 | root: PathBuf, 32 | } 33 | 34 | impl<'a> Context<'a> for CliContext { 35 | /// Return root of our ops 36 | fn root(&'a self) -> &'a PathBuf { 37 | &self.root 38 | } 39 | 40 | /// Run a step command 41 | /// Right now all output is dumped to stdout/stderr 42 | fn run_command(&self, cmd: &mut Command) -> Result<(), installer::steps::Error> { 43 | let status = cmd.spawn()?.wait()?; 44 | if !status.success() { 45 | let program = cmd.get_program().to_string_lossy().into(); 46 | return Err(installer::steps::Error::CommandFailed { program, status }); 47 | } 48 | Ok(()) 49 | } 50 | 51 | /// Run a step command, capture stdout 52 | fn run_command_captured(&self, cmd: &mut Command, input: Option<&str>) -> Result { 53 | cmd.stdin(Stdio::piped()); 54 | cmd.stdout(Stdio::piped()); 55 | cmd.stderr(Stdio::piped()); 56 | let mut ps = cmd.spawn()?; 57 | let mut stdin = ps.stdin.take().expect("stdin failure"); 58 | 59 | if let Some(input) = input { 60 | stdin.write_all(input.as_bytes())?; 61 | } 62 | drop(stdin); 63 | 64 | let output = ps.wait_with_output()?; 65 | Ok(output) 66 | } 67 | } 68 | 69 | /// Ask the user what locale to use 70 | fn ask_locale<'a>(locales: &'a [Locale<'a>]) -> color_eyre::Result<&'a Locale<'a>> { 71 | let locales_disp = locales.iter().enumerate().map(|(i, l)| (i, l, "")).collect::>(); 72 | ensure!(!locales.is_empty(), "Internal error: No locales"); 73 | let index = cliclack::select("Pick a locale") 74 | .items(locales_disp.as_slice()) 75 | .initial_value(0) 76 | .filter_mode() 77 | .set_size(20) 78 | .interact()?; 79 | 80 | Ok(&locales[index]) 81 | } 82 | 83 | fn ask_timezone() -> color_eyre::Result { 84 | let variants = chrono_tz::TZ_VARIANTS 85 | .iter() 86 | .enumerate() 87 | .map(|(i, v)| (i, v, "")) 88 | .collect::>(); 89 | ensure!(!variants.is_empty(), "Internal error: No timezones"); 90 | let index = cliclack::select("Pick a timezone") 91 | .items(variants.as_slice()) 92 | .initial_value(0) 93 | .filter_mode() 94 | .set_size(10) 95 | .interact()?; 96 | 97 | Ok(chrono_tz::TZ_VARIANTS[index].to_string()) 98 | } 99 | 100 | /// Pick an ESP please... 101 | fn ask_esp(parts: &[BootPartition]) -> color_eyre::Result<&BootPartition> { 102 | let parts_disp = parts 103 | .iter() 104 | .enumerate() 105 | .map(|(i, p)| (i, p.to_string(), "")) 106 | .collect::>(); 107 | ensure!( 108 | !parts_disp.is_empty(), 109 | "No disk with an available FAT32-formatted EFI system partition found. Exiting." 110 | ); 111 | let index = cliclack::select("Pick FAT32-formatted ESP + XBOOTLDR partitions") 112 | .items(parts_disp.as_slice()) 113 | .initial_value(0) 114 | .interact()?; 115 | Ok(&parts[index]) 116 | } 117 | 118 | /// Where's it going? 119 | fn ask_rootfs(parts: &[SystemPartition]) -> color_eyre::Result<&SystemPartition> { 120 | let parts_disp = parts 121 | .iter() 122 | .enumerate() 123 | .map(|(i, p)| (i, p.to_string(), "")) 124 | .collect::>(); 125 | ensure!( 126 | !parts_disp.is_empty(), 127 | "No disk with a pre-created Linux partition for the system install root found. Exiting." 128 | ); 129 | let index = cliclack::select("Pick the partition to be used for the system install root (>20GiB)") 130 | .items(parts_disp.as_slice()) 131 | .initial_value(0) 132 | .interact()?; 133 | Ok(&parts[index]) 134 | } 135 | 136 | fn ask_filesystem() -> color_eyre::Result { 137 | let variants = [ 138 | ("xfs", "xfs", "Recommended (fast w/ moss hardlink rollbacks)"), 139 | ( 140 | "f2fs", 141 | "f2fs", 142 | "Not Recommended (surprisingly slow w/ moss hardlink rollbacks)", 143 | ), 144 | ( 145 | "ext4", 146 | "ext4", 147 | "Not Recommended (slow, limited moss hardlink rollback capacity)", 148 | ), 149 | ]; 150 | let index = cliclack::select("Pick a suitable filesystem for the system install root ('/')") 151 | .items(&variants) 152 | .initial_value("xfs") 153 | .interact()?; 154 | Ok(index.into()) 155 | } 156 | 157 | // Grab a password for the root account 158 | fn ask_password() -> color_eyre::Result { 159 | let password = cliclack::password("You'll need to set a default root (administrator) password").interact()?; 160 | let confirmed = cliclack::password("Confirm your password") 161 | .validate_interactively(move |v: &String| { 162 | if *v != password { 163 | return Err("Those passwords do not match"); 164 | } 165 | Ok(()) 166 | }) 167 | .interact()?; 168 | Ok(confirmed) 169 | } 170 | 171 | fn create_user() -> color_eyre::Result { 172 | cliclack::log::info("We now need to create a default (admin) user")?; 173 | let username: String = cliclack::input("Username?").interact()?; 174 | let password = cliclack::password("Pick a password").interact()?; 175 | let confirmed = cliclack::password("Now confirm the password") 176 | .validate_interactively(move |v: &String| { 177 | if *v != password { 178 | return Err("Those passwords do not match"); 179 | } 180 | Ok(()) 181 | }) 182 | .interact()?; 183 | Ok(Account::new(username) 184 | .with_password(confirmed) 185 | .with_shell("/usr/bin/bash")) 186 | } 187 | 188 | fn ask_desktop<'a>(desktops: &'a [&Group]) -> color_eyre::Result<&'a Group> { 189 | let displayable = desktops 190 | .iter() 191 | .enumerate() 192 | .map(|(i, d)| (i, &d.summary, &d.description)) 193 | .collect::>(); 194 | ensure!(!displayable.is_empty(), "Internal error: No displayable desktops"); 195 | let index = cliclack::select("Pick a desktop environment to use") 196 | .items(displayable.as_slice()) 197 | .initial_value(1) 198 | .interact()?; 199 | 200 | Ok(desktops[index]) 201 | } 202 | 203 | fn main() -> color_eyre::Result<()> { 204 | env_logger::init(); 205 | color_eyre::install().unwrap(); 206 | set_colors_enabled(true); 207 | 208 | let euid = unsafe { geteuid() }; 209 | ensure!(euid == 0, "lichen must be run as root. Re-run with sudo."); 210 | 211 | let partition_detection_warning = indoc! {" 212 | This iteration of the installer REQUIRES you to have pre-created GPT partitions. 213 | 214 | It may be a good idea to check the following in gparted (or fdisk) now: 215 | 216 | - The disk you want to use has a GPT partition table 217 | 218 | - The EFI system partition (ESP): 219 | - Is >=256MiB in size 220 | - Has the 'esp' and 'boot' flags set in gparted (corresponds to type 1 in fdisk) 221 | - Has been formatted as FAT32 222 | 223 | - The Linux extended boot partition (XBOOTLDR): 224 | - Is ~4GiB in size, as it is used to store multiple kernels and initramfs images 225 | - Has the flag 'bls_boot' set in gparted (corresponds to type 142 in fdisk) 226 | - Has been formatted as FAT32 227 | 228 | - You have created a partition for your system root (/) 229 | - It has been formatted as XFS (recommended), or another filesystem 230 | recognised by the installer (e.g. ext4 or f2fs). This choice can 231 | be changed later in the installation process. 232 | 233 | - (Optional) You can add a partition to be used for your /home directories 234 | - This is currently not handled by the installer at all, so you need to prepare 235 | and format the partition manually if you want a separate /home partition 236 | "}; 237 | cliclack::log::warning(format!( 238 | "{} This is an alpha quality AerynOS installer.\n\n{}", 239 | style("Warning:").bold(), 240 | partition_detection_warning 241 | ))?; 242 | 243 | cliclack::log::warning("Make sure you have an active internet connection to download packages.")?; 244 | 245 | let should_continue = 246 | cliclack::confirm("Have you set up partitions according to the above requirements?").interact()?; 247 | ensure!(should_continue, "User chose to abort before detecting partitions."); 248 | 249 | cliclack::intro(style("Install AerynOS").bold())?; 250 | 251 | // Test selection management 252 | let selections = selections::Manager::new().with_groups(selections!()); 253 | 254 | let desktops = selections.groups().filter(|g| g.display).collect::>(); 255 | 256 | let sp = cliclack::spinner(); 257 | sp.start("Loading"); 258 | 259 | // Load all the things 260 | let inst = Installer::new()?; 261 | let boots = inst.boot_partitions(); 262 | let parts = inst.system_partitions(); 263 | let locales = inst.locales_for_ids(systemd::localectl_list_locales()?)?; 264 | 265 | ensure!( 266 | !boots.is_empty(), 267 | "Failed to find FAT32-formatted available ESP + XBOOTLDR partitions" 268 | ); 269 | ensure!( 270 | !parts.is_empty(), 271 | "Failed to find an available partition for the system root (/)" 272 | ); 273 | 274 | sp.clear(); 275 | 276 | // TODO: The smart move would be to actually probe the partitions for a valid FS here, 277 | // because we will want to optionally set the partition type and format them 278 | // to the correct fs if this hasn't already been done. 279 | let esp = ask_esp(boots)?; 280 | 281 | let mut rootfs = ask_rootfs(parts)?.clone(); 282 | rootfs.mountpoint = Some("/".into()); 283 | let fs = ask_filesystem()?; 284 | 285 | let selected_desktop = ask_desktop(&desktops)?; 286 | let selected_locale = ask_locale(&locales)?; 287 | let timezone = ask_timezone()?; 288 | let keyboard_layout_warning = indoc! {" 289 | Note that the keyboard layout for the current virtual terminal is controlled 290 | via the Settings application. 291 | 292 | If a new keyboard layout is added there, please be aware that it may be 293 | necessary to exit the installer, open a new virtual terminal, and restart the 294 | installer in the new virtual terminal. 295 | 296 | Otherwise, the desired keyboard layout may not be active when entering user 297 | passwords in the following steps. 298 | "}; 299 | cliclack::log::warning(keyboard_layout_warning)?; 300 | let rootpw = ask_password()?; 301 | let user_account = create_user()?; 302 | 303 | let summary = |title: &str, value: &str| format!("{}: {}", style(title).bold(), value); 304 | 305 | let note = [ 306 | summary("Install", &selected_desktop.summary.to_string()), 307 | summary("Locale", &selected_locale.to_string()), 308 | summary("Timezone", &timezone), 309 | summary("Bootloader", &esp.to_string()), 310 | summary("Root (/) partition", &rootfs.to_string()), 311 | summary("Root (/) filesystem", &fs), 312 | ]; 313 | 314 | cliclack::note("Installation summary", note.join("\n"))?; 315 | 316 | let model = installer::Model { 317 | accounts: [Account::root().with_password(rootpw), user_account].into(), 318 | boot_partition: esp.to_owned(), 319 | partitions: [rootfs.clone()].into(), 320 | locale: Some(selected_locale), 321 | timezone: Some(timezone), 322 | rootfs_type: fs, 323 | packages: selections.selections_with(["develop", &selected_desktop.name, "kernel-desktop"])?, 324 | }; 325 | 326 | let y = cliclack::confirm("Do you want to install?").interact()?; 327 | if !y { 328 | cliclack::outro_cancel("No changes have been made to your system")?; 329 | return Ok(()); 330 | } 331 | 332 | cliclack::outro("Now proceeding with installation")?; 333 | 334 | // TODO: Use proper temp directory 335 | let context = CliContext { 336 | root: "/tmp/lichen".into(), 337 | }; 338 | let (cleanups, steps) = inst.compile_to_steps(&model, &context)?; 339 | let multi = indicatif::MultiProgress::new(); 340 | let total = indicatif::ProgressBar::new(steps.len() as u64 + cleanups.len() as u64).with_style( 341 | ProgressStyle::with_template("\n|{bar:20.cyan/blue}| {pos}/{len}") 342 | .unwrap() 343 | .progress_chars("■≡=- "), 344 | ); 345 | 346 | let total = multi.add(total); 347 | for step in steps { 348 | total.inc(1); 349 | if step.is_indeterminate() { 350 | let progress_bar = multi.insert_before( 351 | &total, 352 | indicatif::ProgressBar::new(1) 353 | .with_message(format!("{} {}", step.title().blue(), step.describe().bold(),)) 354 | .with_style( 355 | ProgressStyle::with_template(" {spinner} {wide_msg} ") 356 | .unwrap() 357 | .tick_chars("--=≡■≡=--"), 358 | ), 359 | ); 360 | progress_bar.enable_steady_tick(Duration::from_millis(150)); 361 | step.execute(&context)?; 362 | } else { 363 | multi.println(format!("{} {}", step.title().blue(), step.describe().bold()))?; 364 | multi.suspend(|| step.execute(&context))?; 365 | } 366 | } 367 | 368 | // Execute all the cleanups 369 | for cleanup in cleanups { 370 | let progress_bar = multi.insert_before( 371 | &total, 372 | indicatif::ProgressBar::new(1) 373 | .with_message(format!("{} {}", cleanup.title().yellow(), cleanup.describe().bold(),)) 374 | .with_style( 375 | ProgressStyle::with_template(" {spinner} {wide_msg} ") 376 | .unwrap() 377 | .tick_chars("--=≡■≡=--"), 378 | ), 379 | ); 380 | progress_bar.enable_steady_tick(Duration::from_millis(150)); 381 | total.inc(1); 382 | cleanup.execute(&context)?; 383 | } 384 | let home_note = indoc!( 385 | " 386 | NOTE: If you reserved space for a separate /home partition above, now would 387 | be a good time to format it with your filesystem of choice, and to 388 | ensure that it is enabled in the /etc/fstab file in the new install. 389 | 390 | Remember to copy/move the new /home/${USER} directory created by the 391 | installer in the / partition to the new /home partition. 392 | " 393 | ); 394 | let installer_success = format!( 395 | "🎉 🥳 Successfully installed {}! Reboot now to start using it!", 396 | style("AerynOS").bold() 397 | ); 398 | multi.clear()?; 399 | println!("\n{home_note}\n{installer_success}\n"); 400 | 401 | Ok(()) 402 | } 403 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /LICENSES/MPL-2.0.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.19" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.11" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.7" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 64 | dependencies = [ 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.9" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell_polyfill", 76 | "windows-sys", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 84 | 85 | [[package]] 86 | name = "backtrace" 87 | version = "0.3.75" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 90 | dependencies = [ 91 | "addr2line", 92 | "cfg-if", 93 | "libc", 94 | "miniz_oxide", 95 | "object", 96 | "rustc-demangle", 97 | "windows-targets", 98 | ] 99 | 100 | [[package]] 101 | name = "bitflags" 102 | version = "2.9.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 105 | dependencies = [ 106 | "serde", 107 | ] 108 | 109 | [[package]] 110 | name = "bumpalo" 111 | version = "3.18.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "1.0.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 120 | 121 | [[package]] 122 | name = "cfg_aliases" 123 | version = "0.2.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 126 | 127 | [[package]] 128 | name = "chrono" 129 | version = "0.4.41" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 132 | dependencies = [ 133 | "num-traits", 134 | ] 135 | 136 | [[package]] 137 | name = "chrono-tz" 138 | version = "0.10.3" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" 141 | dependencies = [ 142 | "chrono", 143 | "chrono-tz-build", 144 | "phf", 145 | ] 146 | 147 | [[package]] 148 | name = "chrono-tz-build" 149 | version = "0.4.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" 152 | dependencies = [ 153 | "parse-zoneinfo", 154 | "phf_codegen", 155 | ] 156 | 157 | [[package]] 158 | name = "cliclack" 159 | version = "0.3.5" 160 | source = "git+https://github.com/ikeycode/cliclack.git?rev=35a1882c601b90bf1398c3cb867cc6b20bbe9ce9#35a1882c601b90bf1398c3cb867cc6b20bbe9ce9" 161 | dependencies = [ 162 | "console", 163 | "indicatif", 164 | "once_cell", 165 | "strsim", 166 | "termsize", 167 | "textwrap", 168 | "zeroize", 169 | ] 170 | 171 | [[package]] 172 | name = "color-eyre" 173 | version = "0.6.5" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" 176 | dependencies = [ 177 | "backtrace", 178 | "color-spantrace", 179 | "eyre", 180 | "indenter", 181 | "once_cell", 182 | "owo-colors", 183 | "tracing-error", 184 | "url", 185 | ] 186 | 187 | [[package]] 188 | name = "color-spantrace" 189 | version = "0.3.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" 192 | dependencies = [ 193 | "once_cell", 194 | "owo-colors", 195 | "tracing-core", 196 | "tracing-error", 197 | ] 198 | 199 | [[package]] 200 | name = "colorchoice" 201 | version = "1.0.4" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 204 | 205 | [[package]] 206 | name = "console" 207 | version = "0.15.11" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 210 | dependencies = [ 211 | "encode_unicode", 212 | "libc", 213 | "once_cell", 214 | "unicode-width", 215 | "windows-sys", 216 | ] 217 | 218 | [[package]] 219 | name = "convert_case" 220 | version = "0.7.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 223 | dependencies = [ 224 | "unicode-segmentation", 225 | ] 226 | 227 | [[package]] 228 | name = "crc" 229 | version = "3.3.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 232 | dependencies = [ 233 | "crc-catalog", 234 | ] 235 | 236 | [[package]] 237 | name = "crc-catalog" 238 | version = "2.4.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 241 | 242 | [[package]] 243 | name = "crossterm" 244 | version = "0.29.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 247 | dependencies = [ 248 | "bitflags", 249 | "crossterm_winapi", 250 | "derive_more", 251 | "document-features", 252 | "futures-core", 253 | "mio", 254 | "parking_lot", 255 | "rustix", 256 | "serde", 257 | "signal-hook", 258 | "signal-hook-mio", 259 | "winapi", 260 | ] 261 | 262 | [[package]] 263 | name = "crossterm_winapi" 264 | version = "0.9.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 267 | dependencies = [ 268 | "winapi", 269 | ] 270 | 271 | [[package]] 272 | name = "derive_more" 273 | version = "2.0.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 276 | dependencies = [ 277 | "derive_more-impl", 278 | ] 279 | 280 | [[package]] 281 | name = "derive_more-impl" 282 | version = "2.0.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 285 | dependencies = [ 286 | "convert_case", 287 | "proc-macro2", 288 | "quote", 289 | "syn", 290 | ] 291 | 292 | [[package]] 293 | name = "dialoguer" 294 | version = "0.11.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 297 | dependencies = [ 298 | "console", 299 | "fuzzy-matcher", 300 | "shell-words", 301 | "tempfile", 302 | "thiserror 1.0.69", 303 | "zeroize", 304 | ] 305 | 306 | [[package]] 307 | name = "displaydoc" 308 | version = "0.2.5" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 311 | dependencies = [ 312 | "proc-macro2", 313 | "quote", 314 | "syn", 315 | ] 316 | 317 | [[package]] 318 | name = "document-features" 319 | version = "0.2.11" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 322 | dependencies = [ 323 | "litrs", 324 | ] 325 | 326 | [[package]] 327 | name = "either" 328 | version = "1.15.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 331 | 332 | [[package]] 333 | name = "encode_unicode" 334 | version = "1.0.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 337 | 338 | [[package]] 339 | name = "env_filter" 340 | version = "0.1.3" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 343 | dependencies = [ 344 | "log", 345 | "regex", 346 | ] 347 | 348 | [[package]] 349 | name = "env_logger" 350 | version = "0.11.8" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 353 | dependencies = [ 354 | "anstream", 355 | "anstyle", 356 | "env_filter", 357 | "jiff", 358 | "log", 359 | ] 360 | 361 | [[package]] 362 | name = "errno" 363 | version = "0.3.12" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 366 | dependencies = [ 367 | "libc", 368 | "windows-sys", 369 | ] 370 | 371 | [[package]] 372 | name = "eyre" 373 | version = "0.6.12" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 376 | dependencies = [ 377 | "indenter", 378 | "once_cell", 379 | ] 380 | 381 | [[package]] 382 | name = "fastrand" 383 | version = "2.3.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 386 | 387 | [[package]] 388 | name = "form_urlencoded" 389 | version = "1.2.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 392 | dependencies = [ 393 | "percent-encoding", 394 | ] 395 | 396 | [[package]] 397 | name = "fs-err" 398 | version = "3.1.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" 401 | dependencies = [ 402 | "autocfg", 403 | "tokio", 404 | ] 405 | 406 | [[package]] 407 | name = "futures-core" 408 | version = "0.3.31" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 411 | 412 | [[package]] 413 | name = "fuzzy-matcher" 414 | version = "0.3.7" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 417 | dependencies = [ 418 | "thread_local", 419 | ] 420 | 421 | [[package]] 422 | name = "getrandom" 423 | version = "0.3.3" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 426 | dependencies = [ 427 | "cfg-if", 428 | "libc", 429 | "r-efi", 430 | "wasi 0.14.2+wasi-0.2.4", 431 | ] 432 | 433 | [[package]] 434 | name = "gimli" 435 | version = "0.31.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 438 | 439 | [[package]] 440 | name = "gpt" 441 | version = "4.1.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "3696fafb1ecdcc2ae3ce337de73e9202806068594b77d22fdf2f3573c5ec2219" 444 | dependencies = [ 445 | "bitflags", 446 | "crc", 447 | "simple-bytes", 448 | "uuid", 449 | ] 450 | 451 | [[package]] 452 | name = "heck" 453 | version = "0.5.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 456 | 457 | [[package]] 458 | name = "human_bytes" 459 | version = "0.4.3" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" 462 | 463 | [[package]] 464 | name = "icu_collections" 465 | version = "2.0.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 468 | dependencies = [ 469 | "displaydoc", 470 | "potential_utf", 471 | "yoke", 472 | "zerofrom", 473 | "zerovec", 474 | ] 475 | 476 | [[package]] 477 | name = "icu_locale_core" 478 | version = "2.0.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 481 | dependencies = [ 482 | "displaydoc", 483 | "litemap", 484 | "tinystr", 485 | "writeable", 486 | "zerovec", 487 | ] 488 | 489 | [[package]] 490 | name = "icu_normalizer" 491 | version = "2.0.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 494 | dependencies = [ 495 | "displaydoc", 496 | "icu_collections", 497 | "icu_normalizer_data", 498 | "icu_properties", 499 | "icu_provider", 500 | "smallvec", 501 | "zerovec", 502 | ] 503 | 504 | [[package]] 505 | name = "icu_normalizer_data" 506 | version = "2.0.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 509 | 510 | [[package]] 511 | name = "icu_properties" 512 | version = "2.0.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 515 | dependencies = [ 516 | "displaydoc", 517 | "icu_collections", 518 | "icu_locale_core", 519 | "icu_properties_data", 520 | "icu_provider", 521 | "potential_utf", 522 | "zerotrie", 523 | "zerovec", 524 | ] 525 | 526 | [[package]] 527 | name = "icu_properties_data" 528 | version = "2.0.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 531 | 532 | [[package]] 533 | name = "icu_provider" 534 | version = "2.0.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 537 | dependencies = [ 538 | "displaydoc", 539 | "icu_locale_core", 540 | "stable_deref_trait", 541 | "tinystr", 542 | "writeable", 543 | "yoke", 544 | "zerofrom", 545 | "zerotrie", 546 | "zerovec", 547 | ] 548 | 549 | [[package]] 550 | name = "idna" 551 | version = "1.0.3" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 554 | dependencies = [ 555 | "idna_adapter", 556 | "smallvec", 557 | "utf8_iter", 558 | ] 559 | 560 | [[package]] 561 | name = "idna_adapter" 562 | version = "1.2.1" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 565 | dependencies = [ 566 | "icu_normalizer", 567 | "icu_properties", 568 | ] 569 | 570 | [[package]] 571 | name = "indenter" 572 | version = "0.3.3" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 575 | 576 | [[package]] 577 | name = "indicatif" 578 | version = "0.17.11" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 581 | dependencies = [ 582 | "console", 583 | "number_prefix", 584 | "portable-atomic", 585 | "unicode-width", 586 | "web-time", 587 | ] 588 | 589 | [[package]] 590 | name = "indoc" 591 | version = "2.0.6" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 594 | 595 | [[package]] 596 | name = "installer" 597 | version = "0.1.0" 598 | dependencies = [ 599 | "fs-err", 600 | "human_bytes", 601 | "itertools", 602 | "libc", 603 | "log", 604 | "serde", 605 | "serde_json", 606 | "system", 607 | "thiserror 2.0.12", 608 | "topology", 609 | ] 610 | 611 | [[package]] 612 | name = "is_terminal_polyfill" 613 | version = "1.70.1" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 616 | 617 | [[package]] 618 | name = "itertools" 619 | version = "0.14.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 622 | dependencies = [ 623 | "either", 624 | ] 625 | 626 | [[package]] 627 | name = "itoa" 628 | version = "1.0.15" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 631 | 632 | [[package]] 633 | name = "jiff" 634 | version = "0.2.15" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" 637 | dependencies = [ 638 | "jiff-static", 639 | "log", 640 | "portable-atomic", 641 | "portable-atomic-util", 642 | "serde", 643 | ] 644 | 645 | [[package]] 646 | name = "jiff-static" 647 | version = "0.2.15" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" 650 | dependencies = [ 651 | "proc-macro2", 652 | "quote", 653 | "syn", 654 | ] 655 | 656 | [[package]] 657 | name = "js-sys" 658 | version = "0.3.77" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 661 | dependencies = [ 662 | "once_cell", 663 | "wasm-bindgen", 664 | ] 665 | 666 | [[package]] 667 | name = "lazy_static" 668 | version = "1.5.0" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 671 | 672 | [[package]] 673 | name = "libc" 674 | version = "0.2.173" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" 677 | 678 | [[package]] 679 | name = "lichen_cli" 680 | version = "0.1.0" 681 | dependencies = [ 682 | "chrono-tz", 683 | "cliclack", 684 | "color-eyre", 685 | "console", 686 | "crossterm", 687 | "dialoguer", 688 | "env_logger", 689 | "indicatif", 690 | "indoc", 691 | "installer", 692 | "nix", 693 | ] 694 | 695 | [[package]] 696 | name = "linux-raw-sys" 697 | version = "0.9.4" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 700 | 701 | [[package]] 702 | name = "litemap" 703 | version = "0.8.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 706 | 707 | [[package]] 708 | name = "litrs" 709 | version = "0.4.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 712 | 713 | [[package]] 714 | name = "lock_api" 715 | version = "0.4.13" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 718 | dependencies = [ 719 | "autocfg", 720 | "scopeguard", 721 | ] 722 | 723 | [[package]] 724 | name = "log" 725 | version = "0.4.27" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 728 | 729 | [[package]] 730 | name = "memchr" 731 | version = "2.7.5" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 734 | 735 | [[package]] 736 | name = "miniz_oxide" 737 | version = "0.8.9" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 740 | dependencies = [ 741 | "adler2", 742 | ] 743 | 744 | [[package]] 745 | name = "mio" 746 | version = "1.0.4" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 749 | dependencies = [ 750 | "libc", 751 | "log", 752 | "wasi 0.11.1+wasi-snapshot-preview1", 753 | "windows-sys", 754 | ] 755 | 756 | [[package]] 757 | name = "nix" 758 | version = "0.30.1" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 761 | dependencies = [ 762 | "bitflags", 763 | "cfg-if", 764 | "cfg_aliases", 765 | "libc", 766 | ] 767 | 768 | [[package]] 769 | name = "num-traits" 770 | version = "0.2.19" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 773 | dependencies = [ 774 | "autocfg", 775 | ] 776 | 777 | [[package]] 778 | name = "number_prefix" 779 | version = "0.4.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 782 | 783 | [[package]] 784 | name = "object" 785 | version = "0.36.7" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 788 | dependencies = [ 789 | "memchr", 790 | ] 791 | 792 | [[package]] 793 | name = "once_cell" 794 | version = "1.21.3" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 797 | 798 | [[package]] 799 | name = "once_cell_polyfill" 800 | version = "1.70.1" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 803 | 804 | [[package]] 805 | name = "owo-colors" 806 | version = "4.2.1" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" 809 | 810 | [[package]] 811 | name = "parking_lot" 812 | version = "0.12.4" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 815 | dependencies = [ 816 | "lock_api", 817 | "parking_lot_core", 818 | ] 819 | 820 | [[package]] 821 | name = "parking_lot_core" 822 | version = "0.9.11" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 825 | dependencies = [ 826 | "cfg-if", 827 | "libc", 828 | "redox_syscall", 829 | "smallvec", 830 | "windows-targets", 831 | ] 832 | 833 | [[package]] 834 | name = "parse-zoneinfo" 835 | version = "0.3.1" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" 838 | dependencies = [ 839 | "regex", 840 | ] 841 | 842 | [[package]] 843 | name = "percent-encoding" 844 | version = "2.3.1" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 847 | 848 | [[package]] 849 | name = "phf" 850 | version = "0.11.3" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 853 | dependencies = [ 854 | "phf_shared", 855 | ] 856 | 857 | [[package]] 858 | name = "phf_codegen" 859 | version = "0.11.3" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 862 | dependencies = [ 863 | "phf_generator", 864 | "phf_shared", 865 | ] 866 | 867 | [[package]] 868 | name = "phf_generator" 869 | version = "0.11.3" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 872 | dependencies = [ 873 | "phf_shared", 874 | "rand", 875 | ] 876 | 877 | [[package]] 878 | name = "phf_shared" 879 | version = "0.11.3" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 882 | dependencies = [ 883 | "siphasher", 884 | ] 885 | 886 | [[package]] 887 | name = "pin-project-lite" 888 | version = "0.2.16" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 891 | 892 | [[package]] 893 | name = "portable-atomic" 894 | version = "1.11.1" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 897 | 898 | [[package]] 899 | name = "portable-atomic-util" 900 | version = "0.2.4" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 903 | dependencies = [ 904 | "portable-atomic", 905 | ] 906 | 907 | [[package]] 908 | name = "potential_utf" 909 | version = "0.1.2" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 912 | dependencies = [ 913 | "zerovec", 914 | ] 915 | 916 | [[package]] 917 | name = "proc-macro2" 918 | version = "1.0.95" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 921 | dependencies = [ 922 | "unicode-ident", 923 | ] 924 | 925 | [[package]] 926 | name = "quote" 927 | version = "1.0.40" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 930 | dependencies = [ 931 | "proc-macro2", 932 | ] 933 | 934 | [[package]] 935 | name = "r-efi" 936 | version = "5.2.0" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 939 | 940 | [[package]] 941 | name = "rand" 942 | version = "0.8.5" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 945 | dependencies = [ 946 | "rand_core", 947 | ] 948 | 949 | [[package]] 950 | name = "rand_core" 951 | version = "0.6.4" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 954 | 955 | [[package]] 956 | name = "redox_syscall" 957 | version = "0.5.13" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 960 | dependencies = [ 961 | "bitflags", 962 | ] 963 | 964 | [[package]] 965 | name = "regex" 966 | version = "1.11.1" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 969 | dependencies = [ 970 | "aho-corasick", 971 | "memchr", 972 | "regex-automata", 973 | "regex-syntax", 974 | ] 975 | 976 | [[package]] 977 | name = "regex-automata" 978 | version = "0.4.9" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 981 | dependencies = [ 982 | "aho-corasick", 983 | "memchr", 984 | "regex-syntax", 985 | ] 986 | 987 | [[package]] 988 | name = "regex-syntax" 989 | version = "0.8.5" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 992 | 993 | [[package]] 994 | name = "rustc-demangle" 995 | version = "0.1.25" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 998 | 999 | [[package]] 1000 | name = "rustix" 1001 | version = "1.0.7" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1004 | dependencies = [ 1005 | "bitflags", 1006 | "errno", 1007 | "libc", 1008 | "linux-raw-sys", 1009 | "windows-sys", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "rustversion" 1014 | version = "1.0.21" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 1017 | 1018 | [[package]] 1019 | name = "ryu" 1020 | version = "1.0.20" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1023 | 1024 | [[package]] 1025 | name = "scopeguard" 1026 | version = "1.2.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1029 | 1030 | [[package]] 1031 | name = "serde" 1032 | version = "1.0.219" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1035 | dependencies = [ 1036 | "serde_derive", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "serde_derive" 1041 | version = "1.0.219" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1044 | dependencies = [ 1045 | "proc-macro2", 1046 | "quote", 1047 | "syn", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "serde_json" 1052 | version = "1.0.140" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1055 | dependencies = [ 1056 | "itoa", 1057 | "memchr", 1058 | "ryu", 1059 | "serde", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "sharded-slab" 1064 | version = "0.1.7" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1067 | dependencies = [ 1068 | "lazy_static", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "shell-words" 1073 | version = "1.1.0" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1076 | 1077 | [[package]] 1078 | name = "signal-hook" 1079 | version = "0.3.18" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 1082 | dependencies = [ 1083 | "libc", 1084 | "signal-hook-registry", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "signal-hook-mio" 1089 | version = "0.2.4" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1092 | dependencies = [ 1093 | "libc", 1094 | "mio", 1095 | "signal-hook", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "signal-hook-registry" 1100 | version = "1.4.5" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1103 | dependencies = [ 1104 | "libc", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "simple-bytes" 1109 | version = "0.2.14" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "c11532d9d241904f095185f35dcdaf930b1427a94d5b01d7002d74ba19b44cc4" 1112 | 1113 | [[package]] 1114 | name = "siphasher" 1115 | version = "1.0.1" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1118 | 1119 | [[package]] 1120 | name = "smallvec" 1121 | version = "1.15.1" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1124 | 1125 | [[package]] 1126 | name = "smawk" 1127 | version = "0.3.2" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 1130 | 1131 | [[package]] 1132 | name = "snafu" 1133 | version = "0.8.6" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" 1136 | dependencies = [ 1137 | "snafu-derive", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "snafu-derive" 1142 | version = "0.8.6" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" 1145 | dependencies = [ 1146 | "heck", 1147 | "proc-macro2", 1148 | "quote", 1149 | "syn", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "stable_deref_trait" 1154 | version = "1.2.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1157 | 1158 | [[package]] 1159 | name = "strsim" 1160 | version = "0.11.1" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1163 | 1164 | [[package]] 1165 | name = "superblock" 1166 | version = "0.1.0" 1167 | source = "git+https://github.com/AerynOS/disks-rs.git?rev=0768fe553b123b2086980bc809011e9786bffd95#0768fe553b123b2086980bc809011e9786bffd95" 1168 | dependencies = [ 1169 | "serde", 1170 | "serde_json", 1171 | "snafu", 1172 | "uuid", 1173 | "zerocopy", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "syn" 1178 | version = "2.0.103" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" 1181 | dependencies = [ 1182 | "proc-macro2", 1183 | "quote", 1184 | "unicode-ident", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "synstructure" 1189 | version = "0.13.2" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1192 | dependencies = [ 1193 | "proc-macro2", 1194 | "quote", 1195 | "syn", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "system" 1200 | version = "0.1.0" 1201 | dependencies = [ 1202 | "fs-err", 1203 | "gpt", 1204 | "serde", 1205 | "serde_json", 1206 | "superblock", 1207 | "thiserror 2.0.12", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "tempfile" 1212 | version = "3.20.0" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1215 | dependencies = [ 1216 | "fastrand", 1217 | "getrandom", 1218 | "once_cell", 1219 | "rustix", 1220 | "windows-sys", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "termsize" 1225 | version = "0.1.9" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "6f11ff5c25c172608d5b85e2fb43ee9a6d683a7f4ab7f96ae07b3d8b590368fd" 1228 | dependencies = [ 1229 | "libc", 1230 | "winapi", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "textwrap" 1235 | version = "0.16.2" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 1238 | dependencies = [ 1239 | "smawk", 1240 | "unicode-linebreak", 1241 | "unicode-width", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "thiserror" 1246 | version = "1.0.69" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1249 | dependencies = [ 1250 | "thiserror-impl 1.0.69", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "thiserror" 1255 | version = "2.0.12" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1258 | dependencies = [ 1259 | "thiserror-impl 2.0.12", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "thiserror-impl" 1264 | version = "1.0.69" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1267 | dependencies = [ 1268 | "proc-macro2", 1269 | "quote", 1270 | "syn", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "thiserror-impl" 1275 | version = "2.0.12" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1278 | dependencies = [ 1279 | "proc-macro2", 1280 | "quote", 1281 | "syn", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "thread_local" 1286 | version = "1.1.9" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1289 | dependencies = [ 1290 | "cfg-if", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "tinystr" 1295 | version = "0.8.1" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1298 | dependencies = [ 1299 | "displaydoc", 1300 | "zerovec", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "tokio" 1305 | version = "1.45.1" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 1308 | dependencies = [ 1309 | "backtrace", 1310 | "pin-project-lite", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "topology" 1315 | version = "0.1.0" 1316 | source = "git+https://github.com/AerynOS/blsforme.git?rev=a09fa783aab22c8a057fc38f827d2fd2d3d157d9#a09fa783aab22c8a057fc38f827d2fd2d3d157d9" 1317 | dependencies = [ 1318 | "fs-err", 1319 | "gpt", 1320 | "log", 1321 | "nix", 1322 | "snafu", 1323 | "superblock", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "tracing" 1328 | version = "0.1.41" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1331 | dependencies = [ 1332 | "pin-project-lite", 1333 | "tracing-core", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "tracing-core" 1338 | version = "0.1.34" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1341 | dependencies = [ 1342 | "once_cell", 1343 | "valuable", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "tracing-error" 1348 | version = "0.2.1" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 1351 | dependencies = [ 1352 | "tracing", 1353 | "tracing-subscriber", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "tracing-subscriber" 1358 | version = "0.3.19" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1361 | dependencies = [ 1362 | "sharded-slab", 1363 | "thread_local", 1364 | "tracing-core", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "unicode-ident" 1369 | version = "1.0.18" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1372 | 1373 | [[package]] 1374 | name = "unicode-linebreak" 1375 | version = "0.1.5" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1378 | 1379 | [[package]] 1380 | name = "unicode-segmentation" 1381 | version = "1.12.0" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1384 | 1385 | [[package]] 1386 | name = "unicode-width" 1387 | version = "0.2.1" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 1390 | 1391 | [[package]] 1392 | name = "url" 1393 | version = "2.5.4" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1396 | dependencies = [ 1397 | "form_urlencoded", 1398 | "idna", 1399 | "percent-encoding", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "utf8_iter" 1404 | version = "1.0.4" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1407 | 1408 | [[package]] 1409 | name = "utf8parse" 1410 | version = "0.2.2" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1413 | 1414 | [[package]] 1415 | name = "uuid" 1416 | version = "1.17.0" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 1419 | dependencies = [ 1420 | "getrandom", 1421 | "js-sys", 1422 | "wasm-bindgen", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "valuable" 1427 | version = "0.1.1" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1430 | 1431 | [[package]] 1432 | name = "wasi" 1433 | version = "0.11.1+wasi-snapshot-preview1" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1436 | 1437 | [[package]] 1438 | name = "wasi" 1439 | version = "0.14.2+wasi-0.2.4" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1442 | dependencies = [ 1443 | "wit-bindgen-rt", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "wasm-bindgen" 1448 | version = "0.2.100" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1451 | dependencies = [ 1452 | "cfg-if", 1453 | "once_cell", 1454 | "rustversion", 1455 | "wasm-bindgen-macro", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "wasm-bindgen-backend" 1460 | version = "0.2.100" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1463 | dependencies = [ 1464 | "bumpalo", 1465 | "log", 1466 | "proc-macro2", 1467 | "quote", 1468 | "syn", 1469 | "wasm-bindgen-shared", 1470 | ] 1471 | 1472 | [[package]] 1473 | name = "wasm-bindgen-macro" 1474 | version = "0.2.100" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1477 | dependencies = [ 1478 | "quote", 1479 | "wasm-bindgen-macro-support", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "wasm-bindgen-macro-support" 1484 | version = "0.2.100" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1487 | dependencies = [ 1488 | "proc-macro2", 1489 | "quote", 1490 | "syn", 1491 | "wasm-bindgen-backend", 1492 | "wasm-bindgen-shared", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "wasm-bindgen-shared" 1497 | version = "0.2.100" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1500 | dependencies = [ 1501 | "unicode-ident", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "web-time" 1506 | version = "1.1.0" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1509 | dependencies = [ 1510 | "js-sys", 1511 | "wasm-bindgen", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "winapi" 1516 | version = "0.3.9" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1519 | dependencies = [ 1520 | "winapi-i686-pc-windows-gnu", 1521 | "winapi-x86_64-pc-windows-gnu", 1522 | ] 1523 | 1524 | [[package]] 1525 | name = "winapi-i686-pc-windows-gnu" 1526 | version = "0.4.0" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1529 | 1530 | [[package]] 1531 | name = "winapi-x86_64-pc-windows-gnu" 1532 | version = "0.4.0" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1535 | 1536 | [[package]] 1537 | name = "windows-sys" 1538 | version = "0.59.0" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1541 | dependencies = [ 1542 | "windows-targets", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "windows-targets" 1547 | version = "0.52.6" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1550 | dependencies = [ 1551 | "windows_aarch64_gnullvm", 1552 | "windows_aarch64_msvc", 1553 | "windows_i686_gnu", 1554 | "windows_i686_gnullvm", 1555 | "windows_i686_msvc", 1556 | "windows_x86_64_gnu", 1557 | "windows_x86_64_gnullvm", 1558 | "windows_x86_64_msvc", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "windows_aarch64_gnullvm" 1563 | version = "0.52.6" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1566 | 1567 | [[package]] 1568 | name = "windows_aarch64_msvc" 1569 | version = "0.52.6" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1572 | 1573 | [[package]] 1574 | name = "windows_i686_gnu" 1575 | version = "0.52.6" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1578 | 1579 | [[package]] 1580 | name = "windows_i686_gnullvm" 1581 | version = "0.52.6" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1584 | 1585 | [[package]] 1586 | name = "windows_i686_msvc" 1587 | version = "0.52.6" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1590 | 1591 | [[package]] 1592 | name = "windows_x86_64_gnu" 1593 | version = "0.52.6" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1596 | 1597 | [[package]] 1598 | name = "windows_x86_64_gnullvm" 1599 | version = "0.52.6" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1602 | 1603 | [[package]] 1604 | name = "windows_x86_64_msvc" 1605 | version = "0.52.6" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1608 | 1609 | [[package]] 1610 | name = "wit-bindgen-rt" 1611 | version = "0.39.0" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1614 | dependencies = [ 1615 | "bitflags", 1616 | ] 1617 | 1618 | [[package]] 1619 | name = "writeable" 1620 | version = "0.6.1" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1623 | 1624 | [[package]] 1625 | name = "yoke" 1626 | version = "0.8.0" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1629 | dependencies = [ 1630 | "serde", 1631 | "stable_deref_trait", 1632 | "yoke-derive", 1633 | "zerofrom", 1634 | ] 1635 | 1636 | [[package]] 1637 | name = "yoke-derive" 1638 | version = "0.8.0" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1641 | dependencies = [ 1642 | "proc-macro2", 1643 | "quote", 1644 | "syn", 1645 | "synstructure", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "zerocopy" 1650 | version = "0.8.25" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 1653 | dependencies = [ 1654 | "zerocopy-derive", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "zerocopy-derive" 1659 | version = "0.8.25" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 1662 | dependencies = [ 1663 | "proc-macro2", 1664 | "quote", 1665 | "syn", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "zerofrom" 1670 | version = "0.1.6" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1673 | dependencies = [ 1674 | "zerofrom-derive", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "zerofrom-derive" 1679 | version = "0.1.6" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1682 | dependencies = [ 1683 | "proc-macro2", 1684 | "quote", 1685 | "syn", 1686 | "synstructure", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "zeroize" 1691 | version = "1.8.1" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1694 | dependencies = [ 1695 | "zeroize_derive", 1696 | ] 1697 | 1698 | [[package]] 1699 | name = "zeroize_derive" 1700 | version = "1.4.2" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 1703 | dependencies = [ 1704 | "proc-macro2", 1705 | "quote", 1706 | "syn", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "zerotrie" 1711 | version = "0.2.2" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1714 | dependencies = [ 1715 | "displaydoc", 1716 | "yoke", 1717 | "zerofrom", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "zerovec" 1722 | version = "0.11.2" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 1725 | dependencies = [ 1726 | "yoke", 1727 | "zerofrom", 1728 | "zerovec-derive", 1729 | ] 1730 | 1731 | [[package]] 1732 | name = "zerovec-derive" 1733 | version = "0.11.1" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1736 | dependencies = [ 1737 | "proc-macro2", 1738 | "quote", 1739 | "syn", 1740 | ] 1741 | --------------------------------------------------------------------------------