Compare commits

..

2 Commits

Author SHA1 Message Date
Aiden McClelland
04196df976 fix getNext 2025-12-19 12:35:06 -07:00
Matt Hill
30b1654666 random subnet and also 80 to 5443 2025-12-19 09:59:12 -07:00
853 changed files with 17552 additions and 26644 deletions

View File

@@ -1,5 +0,0 @@
{
"attribution": {
"commit": ""
}
}

View File

@@ -1,81 +0,0 @@
name: Setup Build Environment
description: Common build environment setup steps
inputs:
nodejs-version:
description: Node.js version
required: true
setup-python:
description: Set up Python
required: false
default: "false"
setup-docker:
description: Set up Docker QEMU and Buildx
required: false
default: "true"
setup-sccache:
description: Configure sccache for GitHub Actions
required: false
default: "true"
free-space:
description: Remove unnecessary packages to free disk space
required: false
default: "true"
runs:
using: composite
steps:
- name: Free disk space
if: inputs.free-space == 'true'
shell: bash
run: |
sudo apt-get remove --purge -y azure-cli || true
sudo apt-get remove --purge -y firefox || true
sudo apt-get remove --purge -y ghc-* || true
sudo apt-get remove --purge -y google-cloud-sdk || true
sudo apt-get remove --purge -y google-chrome-stable || true
sudo apt-get remove --purge -y powershell || true
sudo apt-get remove --purge -y php* || true
sudo apt-get remove --purge -y ruby* || true
sudo apt-get remove --purge -y mono-* || true
sudo apt-get autoremove -y
sudo apt-get clean
sudo rm -rf /usr/lib/jvm
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/share/swift
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# BuildJet runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
- name: Ensure hostedtoolcache exists
shell: bash
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
- name: Set up Python
if: inputs.setup-python == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.nodejs-version }}
cache: npm
cache-dependency-path: "**/package-lock.json"
- name: Set up Docker QEMU
if: inputs.setup-docker == 'true'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: inputs.setup-docker == 'true'
uses: docker/setup-buildx-action@v3
- name: Configure sccache
if: inputs.setup-sccache == 'true'
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');

View File

@@ -37,10 +37,6 @@ on:
- master
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -48,7 +44,6 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -65,15 +60,50 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Mount tmpfs
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with:
nodejs-version: ${{ env.NODEJS_VERSION }}
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: TARGET=${{ matrix.triple }} make cli

View File

@@ -1,4 +1,4 @@
name: start-registry
name: Start-Registry
on:
workflow_call:
@@ -35,10 +35,6 @@ on:
- master
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -46,7 +42,6 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -61,15 +56,50 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Mount tmpfs
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with:
nodejs-version: ${{ env.NODEJS_VERSION }}
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: make registry-deb

View File

@@ -1,4 +1,4 @@
name: start-tunnel
name: Start-Tunnel
on:
workflow_call:
@@ -35,10 +35,6 @@ on:
- master
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -46,7 +42,6 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -61,15 +56,50 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Mount tmpfs
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with:
nodejs-version: ${{ env.NODEJS_VERSION }}
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: make tunnel-deb

View File

@@ -27,7 +27,7 @@ on:
- x86_64-nonfree
- aarch64
- aarch64-nonfree
# - raspberrypi
- raspberrypi
- riscv64
deploy:
type: choice
@@ -45,10 +45,6 @@ on:
- master
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -56,7 +52,6 @@ env:
jobs:
compile:
name: Compile Base Binaries
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -91,16 +86,57 @@ jobs:
)[github.event.inputs.runner == 'fast']
}}
steps:
- name: Mount tmpfs
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y azure-cli || true
sudo apt-get remove --purge -y firefox || true
sudo apt-get remove --purge -y ghc-* || true
sudo apt-get remove --purge -y google-cloud-sdk || true
sudo apt-get remove --purge -y google-chrome-stable || true
sudo apt-get remove --purge -y powershell || true
sudo apt-get remove --purge -y php* || true
sudo apt-get remove --purge -y ruby* || true
sudo apt-get remove --purge -y mono-* || true
sudo apt-get autoremove -y
sudo apt-get clean
sudo rm -rf /usr/lib/jvm # All JDKs
sudo rm -rf /usr/local/.ghcup # Haskell toolchain
sudo rm -rf /usr/local/lib/android # Android SDK/NDK, emulator
sudo rm -rf /usr/share/dotnet # .NET SDKs
sudo rm -rf /usr/share/swift # Swift toolchain (if present)
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: ./.github/actions/setup-build
- name: Set up Python
uses: actions/setup-python@v5
with:
nodejs-version: ${{ env.NODEJS_VERSION }}
setup-python: "true"
python-version: "3.x"
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up system dependencies
run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container squashfuse
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
@@ -118,14 +154,13 @@ jobs:
strategy:
fail-fast: false
matrix:
# TODO: re-add "raspberrypi" to the platform list below
platform: >-
${{
fromJson(
format(
'[
["{0}"],
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"]
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64", "raspberrypi"]
]',
github.event.inputs.platform || 'ALL'
)
@@ -189,17 +224,32 @@ jobs:
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
if: ${{ github.event.inputs.runner != 'fast' }}
# BuildJet runners lack /opt/hostedtoolcache, which setup-qemu expects
- name: Ensure hostedtoolcache exists
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y qemu-user-static
wget https://deb.debian.org/debian/pool/main/d/debspawn/debspawn_0.6.2-1_all.deb
sha256sum ./debspawn_0.6.2-1_all.deb | grep 37ef27458cb1e35e8bce4d4f639b06b4b3866fc0b9191ec6b9bd157afd06a817
sudo apt-get install -y ./debspawn_0.6.2-1_all.deb
- name: Configure debspawn
run: |
sudo mkdir -p /etc/debspawn/
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
sudo mkdir -p /var/tmp/debspawn
- run: sudo mount -t tmpfs tmpfs /var/tmp/debspawn
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }}
- name: Download compiled artifacts
uses: actions/download-artifact@v4
with:
@@ -212,19 +262,22 @@ jobs:
run: |
mkdir -p web/node_modules
mkdir -p web/dist/raw
mkdir -p core/bindings
mkdir -p core/startos/bindings
mkdir -p sdk/base/lib/osBindings
mkdir -p container-runtime/node_modules
mkdir -p container-runtime/dist
mkdir -p container-runtime/dist/node_modules
mkdir -p core/startos/bindings
mkdir -p sdk/dist
mkdir -p sdk/baseDist
mkdir -p patch-db/client/node_modules
mkdir -p patch-db/client/dist
mkdir -p web/.angular
mkdir -p web/dist/raw/ui
mkdir -p web/dist/raw/install-wizard
mkdir -p web/dist/raw/setup-wizard
mkdir -p web/dist/static/ui
mkdir -p web/dist/static/install-wizard
mkdir -p web/dist/static/setup-wizard
PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar
@@ -254,3 +307,40 @@ jobs:
name: ${{ matrix.platform }}.img
path: results/*.img
if: ${{ matrix.platform == 'raspberrypi' }}
- name: Upload OTA to registry
run: >-
PLATFORM=${{ matrix.platform }} make upload-ota TARGET="${{
fromJson('{
"alpha": "alpha-registry-x.start9.com",
"beta": "beta-registry.start9.com",
}')[github.event.inputs.deploy]
}}" KEY="${{
fromJson(
format('{{
"alpha": "{0}",
"beta": "{1}",
}}', secrets.ALPHA_INDEX_KEY, secrets.BETA_INDEX_KEY)
)[github.event.inputs.deploy]
}}"
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
index:
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
needs: [image]
runs-on: ubuntu-latest
steps:
- run: >-
curl "https://${{
fromJson('{
"alpha": "alpha-registry-x.start9.com",
"beta": "beta-registry.start9.com",
}')[github.event.inputs.deploy]
}}:8443/resync.cgi?key=${{
fromJson(
format('{{
"alpha": "{0}",
"beta": "{1}",
}}', secrets.ALPHA_INDEX_KEY, secrets.BETA_INDEX_KEY)
)[github.event.inputs.deploy]
}}"

View File

@@ -10,10 +10,6 @@ on:
- master
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: dev-unstable
@@ -21,18 +17,15 @@ env:
jobs:
test:
name: Run Automated Tests
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with:
nodejs-version: ${{ env.NODEJS_VERSION }}
free-space: "false"
setup-docker: "false"
setup-sccache: "false"
node-version: ${{ env.NODEJS_VERSION }}
- name: Build And Run Tests
run: make test

26
.gitignore vendored
View File

@@ -1,24 +1,28 @@
.DS_Store
.idea
*.img
*.img.gz
*.img.xz
*.zip
/*.img
/*.img.gz
/*.img.xz
/*-raspios-bullseye-arm64-lite.img
/*-raspios-bullseye-arm64-lite.zip
/product_key.txt
/*_product_key.txt
.vscode/settings.json
deploy_web.sh
deploy_web.sh
secrets.db
.vscode/
/build/env/*.txt
*.deb
/cargo-deps/**/*
/PLATFORM.txt
/ENVIRONMENT.txt
/GIT_HASH.txt
/VERSION.txt
/*.deb
/target
*.squashfs
/*.squashfs
/results
/dpkg-workdir
/compiled.tar
/compiled-*.tar
/build/lib/firmware
tmp
web/.i18n-checked
agents/USER.md
/firmware
/tmp

146
CLAUDE.md
View File

@@ -1,146 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
StartOS is an open-source Linux distribution for running personal servers. It manages discovery, installation, network configuration, backups, and health monitoring of self-hosted services.
**Tech Stack:**
- Backend: Rust (async/Tokio, Axum web framework)
- Frontend: Angular 20 + TypeScript + TaigaUI
- Container runtime: Node.js/TypeScript with LXC
- Database/State: Patch-DB (git submodule) - storage layer with reactive frontend sync
- API: JSON-RPC via rpc-toolkit (see `agents/rpc-toolkit.md`)
- Auth: Password + session cookie, public/private key signatures, local authcookie (see `core/src/middleware/auth/`)
## Build & Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for:
- Environment setup and requirements
- Build commands and make targets
- Testing and formatting commands
- Environment variables
**Quick reference:**
```bash
. ./devmode.sh # Enable dev mode
make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
make test-core # Run Rust tests
```
## Architecture
### Core (`/core`)
The Rust backend daemon. Main binaries:
- `startbox` - Main daemon (runs as `startd`)
- `start-cli` - CLI interface
- `start-container` - Runs inside LXC containers; communicates with host and manages subcontainers
- `registrybox` - Registry daemon
- `tunnelbox` - VPN/tunnel daemon
**Key modules:**
- `src/context/` - Context types (RpcContext, CliContext, InitContext, DiagnosticContext)
- `src/service/` - Service lifecycle management with actor pattern (`service_actor.rs`)
- `src/db/model/` - Patch-DB models (`public.rs` synced to frontend, `private.rs` backend-only)
- `src/net/` - Networking (DNS, ACME, WiFi, Tor via Arti, WireGuard)
- `src/s9pk/` - S9PK package format (merkle archive)
- `src/registry/` - Package registry management
**RPC Pattern:** See `agents/rpc-toolkit.md`
### Web (`/web`)
Angular projects sharing common code:
- `projects/ui/` - Main admin interface
- `projects/setup-wizard/` - Initial setup
- `projects/start-tunnel/` - VPN management UI
- `projects/shared/` - Common library (API clients, components)
- `projects/marketplace/` - Service discovery
**Development:**
```bash
cd web
npm ci
npm run start:ui # Dev server with mocks
npm run build:ui # Production build
npm run check # Type check all projects
```
### Container Runtime (`/container-runtime`)
Node.js runtime that manages service containers via RPC. See `RPCSpec.md` for protocol.
**Container Architecture:**
```
LXC Container (uniform base for all services)
└── systemd
└── container-runtime.service
└── Loads /usr/lib/startos/package/index.js (from s9pk javascript.squashfs)
└── Package JS launches subcontainers (from images in s9pk)
```
The container runtime communicates with the host via JSON-RPC over Unix socket. Package JavaScript must export functions conforming to the `ABI` type defined in `sdk/base/lib/types.ts`.
**`/media/startos/` directory (mounted by host into container):**
| Path | Description |
|------|-------------|
| `volumes/<name>/` | Package data volumes (id-mapped, persistent) |
| `assets/` | Read-only assets from s9pk `assets.squashfs` |
| `images/<name>/` | Container images (squashfs, used for subcontainers) |
| `images/<name>.env` | Environment variables for image |
| `images/<name>.json` | Image metadata |
| `backup/` | Backup mount point (mounted during backup operations) |
| `rpc/service.sock` | RPC socket (container runtime listens here) |
| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) |
**S9PK Structure:** See `agents/s9pk-structure.md`
### SDK (`/sdk`)
TypeScript SDK for packaging services (`@start9labs/start-sdk`).
- `base/` - Core types, ABI definitions, effects interface (`@start9labs/start-sdk-base`)
- `package/` - Full SDK for package developers, re-exports base
### Patch-DB (`/patch-db`)
Git submodule providing diff-based state synchronization. Changes to `db/model/public.rs` automatically sync to the frontend.
**Key patterns:**
- `db.peek().await` - Get a read-only snapshot of the database state
- `db.mutate(|db| { ... }).await` - Apply mutations atomically, returns `MutateResult`
- `#[derive(HasModel)]` - Derive macro for types stored in the database, generates typed accessors
**Generated accessor types** (from `HasModel` derive):
- `as_field()` - Immutable reference: `&Model<T>`
- `as_field_mut()` - Mutable reference: `&mut Model<T>`
- `into_field()` - Owned value: `Model<T>`
**`Model<T>` APIs** (from `db/prelude.rs`):
- `.de()` - Deserialize to `T`
- `.ser(&value)` - Serialize from `T`
- `.mutate(|v| ...)` - Deserialize, mutate, reserialize
- For maps: `.keys()`, `.as_idx(&key)`, `.as_idx_mut(&key)`, `.insert()`, `.remove()`, `.contains_key()`
## Supplementary Documentation
The `agents/` directory contains detailed documentation for AI assistants:
- `TODO.md` - Pending tasks for AI agents (check this first, remove items when completed)
- `USER.md` - Current user identifier (gitignored, see below)
- `rpc-toolkit.md` - JSON-RPC patterns and handler configuration
- `core-rust-patterns.md` - Common utilities and patterns for Rust code in `/core` (guard pattern, mount guards, etc.)
- `s9pk-structure.md` - S9PK package format structure
- `i18n-patterns.md` - Internationalization key conventions and usage in `/core`
### Session Startup
On startup:
1. **Check for `agents/USER.md`** - If it doesn't exist, prompt the user for their name/identifier and create it. This file is gitignored since it varies per developer.
2. **Check `agents/TODO.md` for relevant tasks** - Show TODOs that either:
- Have no `@username` tag (relevant to everyone)
- Are tagged with the current user's identifier
Skip TODOs tagged with a different user.
3. **Ask "What would you like to do today?"** - Offer options for each relevant TODO item, plus "Something else" for other requests.

View File

@@ -11,190 +11,123 @@ This guide is for contributing to the StartOS. If you are interested in packagin
```bash
/
├── assets/ # Screenshots for README
├── build/ # Auxiliary files and scripts for deployed images
├── container-runtime/ # Node.js program managing package containers
├── core/ # Rust backend: API, daemon (startd), CLI (start-cli)
├── debian/ # Debian package maintainer scripts
├── image-recipe/ # Scripts for building StartOS images
├── patch-db/ # (submodule) Diff-based data store for frontend sync
├── sdk/ # TypeScript SDK for building StartOS packages
└── web/ # Web UIs (Angular)
├── assets/
├── container-runtime/
├── core/
├── build/
├── debian/
├── web/
├── image-recipe/
├── patch-db
└── sdk/
```
See component READMEs for details:
- [`core`](core/README.md)
- [`web`](web/README.md)
- [`build`](build/README.md)
- [`patch-db`](https://github.com/Start9Labs/patch-db)
#### assets
screenshots for the StartOS README
#### container-runtime
A NodeJS program that dynamically loads maintainer scripts and communicates with the OS to manage packages
#### core
An API, daemon (startd), and CLI (start-cli) that together provide the core functionality of StartOS.
#### build
Auxiliary files and scripts to include in deployed StartOS images
#### debian
Maintainer scripts for the StartOS Debian package
#### web
Web UIs served under various conditions and used to interact with StartOS APIs.
#### image-recipe
Scripts for building StartOS images
#### patch-db (submodule)
A diff based data store used to synchronize data between the web interfaces and server.
#### sdk
A typescript sdk for building start-os packages
## Environment Setup
#### Clone the StartOS repository
```sh
git clone https://github.com/Start9Labs/start-os.git --recurse-submodules
cd start-os
```
### Development Mode
#### Continue to your project of interest for additional instructions:
For faster iteration during development:
```sh
. ./devmode.sh
```
This sets `ENVIRONMENT=dev` and `GIT_BRANCH_AS_HASH=1` to prevent rebuilds on every commit.
- [`core`](core/README.md)
- [`web-interfaces`](web-interfaces/README.md)
- [`build`](build/README.md)
- [`patch-db`](https://github.com/Start9Labs/patch-db)
## Building
All builds can be performed on any operating system that can run Docker.
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components.
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build
### Requirements
- [GNU Make](https://www.gnu.org/software/make/)
- [Docker](https://docs.docker.com/get-docker/) or [Podman](https://podman.io/)
- [Docker](https://docs.docker.com/get-docker/)
- [NodeJS v20.16.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
- [Rust](https://rustup.rs/) (nightly for formatting)
- [sed](https://www.gnu.org/software/sed/), [grep](https://www.gnu.org/software/grep/), [awk](https://www.gnu.org/software/gawk/)
- [sed](https://www.gnu.org/software/sed/)
- [grep](https://www.gnu.org/software/grep/)
- [awk](https://www.gnu.org/software/gawk/)
- [jq](https://jqlang.github.io/jq/)
- [gzip](https://www.gnu.org/software/gzip/), [brotli](https://github.com/google/brotli)
- [gzip](https://www.gnu.org/software/gzip/)
- [brotli](https://github.com/google/brotli)
### Environment Variables
### Environment variables
| Variable | Description |
|----------|-------------|
| `PLATFORM` | Target platform: `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `riscv64`, `raspberrypi` |
| `ENVIRONMENT` | Hyphen-separated feature flags (see below) |
| `PROFILE` | Build profile: `release` (default) or `dev` |
| `GIT_BRANCH_AS_HASH` | Set to `1` to use git branch name as version hash (avoids rebuilds) |
- `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi`
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
- `ENVIRONMENT`: a hyphen separated set of feature flags to enable
- `dev`: enables password ssh (INSECURE!) and does not compress frontends
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
- `docker`: use `docker` instead of `podman`
- `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit
**ENVIRONMENT flags:**
- `dev` - Enables password SSH before setup, skips frontend compression
- `unstable` - Enables assertions and debugging with performance penalty
- `console` - Enables tokio-console for async debugging
**Platform notes:**
- `-nonfree` variants include proprietary firmware and drivers
- `raspberrypi` includes non-free components by necessity
- Platform is remembered between builds if not specified
### Make Targets
#### Building
| Target | Description |
|--------|-------------|
| `iso` | Create full `.iso` image (not for raspberrypi) |
| `img` | Create full `.img` image (raspberrypi only) |
| `deb` | Build Debian package |
| `all` | Build all Rust binaries |
| `uis` | Build all web UIs |
| `ui` | Build main UI only |
| `ts-bindings` | Generate TypeScript bindings from Rust types |
#### Deploying to Device
For devices on the same network:
| Target | Description |
|--------|-------------|
| `update-startbox REMOTE=start9@<ip>` | Deploy binary + UI only (fastest) |
| `update-deb REMOTE=start9@<ip>` | Deploy full Debian package |
| `update REMOTE=start9@<ip>` | OTA-style update |
| `reflash REMOTE=start9@<ip>` | Reflash as if using live ISO |
| `update-overlay REMOTE=start9@<ip>` | Deploy to in-memory overlay (reverts on reboot) |
For devices on different networks (uses [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)):
| Target | Description |
|--------|-------------|
| `wormhole` | Send startbox binary |
| `wormhole-deb` | Send Debian package |
| `wormhole-squashfs` | Send squashfs image |
#### Other
| Target | Description |
|--------|-------------|
| `format` | Run code formatting (Rust nightly required) |
| `test` | Run all automated tests |
| `test-core` | Run Rust tests |
| `test-sdk` | Run SDK tests |
| `test-container-runtime` | Run container runtime tests |
| `clean` | Delete all compiled artifacts |
## Testing
```bash
make test # All tests
make test-core # Rust tests (via ./core/run-tests.sh)
make test-sdk # SDK tests
make test-container-runtime # Container runtime tests
# Run specific Rust test
cd core && cargo test <test_name> --features=test
```
## Code Formatting
```bash
# Rust (requires nightly)
make format
# TypeScript/HTML/SCSS (web)
cd web && npm run format
```
## Code Style Guidelines
### Formatting
Run the formatters before committing. Configuration is handled by `rustfmt.toml` (Rust) and prettier configs (TypeScript).
### Documentation & Comments
**Rust:**
- Add doc comments (`///`) to public APIs, structs, and non-obvious functions
- Use `//` comments sparingly for complex logic that isn't self-evident
- Prefer self-documenting code (clear naming, small functions) over comments
**TypeScript:**
- Document exported functions and complex types with JSDoc
- Keep comments focused on "why" rather than "what"
**General:**
- Don't add comments that just restate the code
- Update or remove comments when code changes
- TODOs should include context: `// TODO(username): reason`
### Commit Messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
**Types:**
- `feat` - New feature
- `fix` - Bug fix
- `docs` - Documentation only
- `style` - Formatting, no code change
- `refactor` - Code change that neither fixes a bug nor adds a feature
- `test` - Adding or updating tests
- `chore` - Build process, dependencies, etc.
**Examples:**
```
feat(web): add dark mode toggle
fix(core): resolve race condition in service startup
docs: update CONTRIBUTING.md with style guidelines
refactor(sdk): simplify package validation logic
```
### Useful Make Targets
- `iso`: Create a full `.iso` image
- Only possible from Debian
- Not available for `PLATFORM=raspberrypi`
- Additional Requirements:
- [debspawn](https://github.com/lkhq/debspawn)
- `img`: Create a full `.img` image
- Only possible from Debian
- Only available for `PLATFORM=raspberrypi`
- Additional Requirements:
- [debspawn](https://github.com/lkhq/debspawn)
- `format`: Run automatic code formatting for the project
- Additional Requirements:
- [rust](https://rustup.rs/)
- `test`: Run automated tests for the project
- Additional Requirements:
- [rust](https://rustup.rs/)
- `update`: Deploy the current working project to a device over ssh as if through an over-the-air update
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it
- WARNING: changes will be reverted after the device is rebooted
- WARNING: changes to `init` will not take effect as the device is already initialized
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
- When the build it complete will emit a command to paste into the shell of the device to upgrade it
- Additional Requirements:
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
- `clean`: Delete all compiled artifacts

169
Makefile
View File

@@ -1,44 +1,45 @@
ls-files = $(shell git ls-files --cached --others --exclude-standard $1)
PROFILE = release
PLATFORM_FILE := $(shell ./build/env/check-platform.sh)
ENVIRONMENT_FILE := $(shell ./build/env/check-environment.sh)
GIT_HASH_FILE := $(shell ./build/env/check-git-hash.sh)
VERSION_FILE := $(shell ./build/env/check-version.sh)
BASENAME := $(shell PROJECT=startos ./build/env/basename.sh)
PLATFORM := $(shell if [ -f $(PLATFORM_FILE) ]; then cat $(PLATFORM_FILE); else echo unknown; fi)
PLATFORM_FILE := $(shell ./check-platform.sh)
ENVIRONMENT_FILE := $(shell ./check-environment.sh)
GIT_HASH_FILE := $(shell ./check-git-hash.sh)
VERSION_FILE := $(shell ./check-version.sh)
BASENAME := $(shell PROJECT=startos ./basename.sh)
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi)
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./build/env/basename.sh)
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./build/env/basename.sh)
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./basename.sh)
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./basename.sh)
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html
FIRMWARE_ROMS := build/lib/firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./build/lib/firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
STARTD_SRC := core/startd.service $(BUILD_SRC)
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html
FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
BUILD_SRC := $(call ls-files, build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
IMAGE_RECIPE_SRC := $(call ls-files, image-recipe/)
STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
WEB_SHARED_SRC := $(call ls-files, web/projects/shared) $(call ls-files, web/projects/marketplace) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
WEB_UI_SRC := $(call ls-files, web/projects/ui)
WEB_SETUP_WIZARD_SRC := $(call ls-files, web/projects/setup-wizard)
WEB_INSTALL_WIZARD_SRC := $(call ls-files, web/projects/install-wizard)
WEB_START_TUNNEL_SRC := $(call ls-files, web/projects/start-tunnel)
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
GZIP_BIN := $(shell which pigz || which gzip)
TAR_BIN := $(shell which gtar || which tar)
COMPILED_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container container-runtime/rootfs.$(ARCH).squashfs
STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
$(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \
echo target/aarch64-unknown-linux-musl/release/pi-beep; \
echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \
fi) \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
echo target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
fi') \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
echo target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
fi')
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/start-tunneld.service
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
ifeq ($(REMOTE),)
mkdir = mkdir -p $1
@@ -72,7 +73,7 @@ metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
clean:
rm -rf core/target
rm -rf core/bindings
rm -rf core/startos/bindings
rm -rf web/.angular
rm -f web/config.json
rm -rf web/node_modules
@@ -80,7 +81,7 @@ clean:
rm -rf patch-db/client/node_modules
rm -rf patch-db/client/dist
rm -rf patch-db/target
rm -rf target
rm -rf cargo-deps
rm -rf dpkg-workdir
rm -rf image-recipe/deb
rm -rf results
@@ -88,8 +89,14 @@ clean:
rm -rf container-runtime/dist
rm -rf container-runtime/node_modules
rm -f container-runtime/*.squashfs
if [ -d container-runtime/tmp/combined ] && mountpoint container-runtime/tmp/combined; then sudo umount container-runtime/tmp/combined; fi
if [ -d container-runtime/tmp/lower ] && mountpoint container-runtime/tmp/lower; then sudo umount container-runtime/tmp/lower; fi
rm -rf container-runtime/tmp
(cd sdk && make clean)
rm -f env/*.txt
rm -f ENVIRONMENT.txt
rm -f PLATFORM.txt
rm -f GIT_HASH.txt
rm -f VERSION.txt
format:
cd core && cargo +nightly fmt
@@ -106,10 +113,10 @@ test-container-runtime: container-runtime/node_modules/.package-lock.json $(call
cd container-runtime && npm test
install-cli: $(GIT_HASH_FILE)
./core/build/build-cli.sh --install
./core/build-cli.sh --install
cli: $(GIT_HASH_FILE)
./core/build/build-cli.sh
./core/build-cli.sh
registry: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox
@@ -120,49 +127,49 @@ install-registry: $(REGISTRY_TARGETS)
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
$(call cp,core/startos/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE)
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-registrybox.sh
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-registrybox.sh
tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/start-tunneld.service
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
$(call cp,core/startos/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
$(call mkdir,$(DESTDIR)/usr/lib/startos/scripts)
$(call cp,build/lib/scripts/forward-port,$(DESTDIR)/usr/lib/startos/scripts/forward-port)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) web/dist/static/start-tunnel/index.html
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-tunnelbox.sh
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-tunnelbox.sh
deb: results/$(BASENAME).deb
results/$(BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
results/$(BASENAME).deb: dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
registry-deb: results/$(REGISTRY_BASENAME).deb
results/$(REGISTRY_BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
results/$(REGISTRY_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
tunnel-deb: results/$(TUNNEL_BASENAME).deb
results/$(TUNNEL_BASENAME).deb: debian/dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS) build/lib/scripts/forward-port
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables,conntrack ./build/os-compat/run-compat.sh ./debian/dpkg-build.sh
results/$(TUNNEL_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS) build/lib/scripts/forward-port
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables,conntrack ./build/os-compat/run-compat.sh ./dpkg-build.sh
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
squashfs: results/$(BASENAME).squashfs
results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_SRC) results/$(BASENAME).deb
ARCH=$(ARCH) ./build/image-recipe/run-local-build.sh "results/$(BASENAME).deb"
./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
# For creating os images. DO NOT USE
install: $(STARTOS_TARGETS)
@@ -171,18 +178,18 @@ install: $(STARTOS_TARGETS)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,target/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
fi
$(call cp,target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
$(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
$(call mkdir,$(DESTDIR)/usr/lib)
$(call rm,$(DESTDIR)/usr/lib/startos)
@@ -190,16 +197,18 @@ install: $(STARTOS_TARGETS)
$(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime)
$(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs)
$(call cp,build/env/PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
$(call cp,build/env/ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
$(call cp,build/env/GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
$(call cp,build/env/VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
$(call cp,PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
$(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
$(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware)
update-overlay: $(STARTOS_TARGETS)
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
@if [ "`ssh $(REMOTE) 'cat /usr/lib/startos/VERSION.txt'`" != "`cat $(VERSION_FILE)`" ]; then >&2 echo "StartOS requires migrations: update-overlay is unavailable." && false; fi
@if [ "`ssh $(REMOTE) 'cat /usr/lib/startos/VERSION.txt'`" != "`cat ./VERSION.txt`" ]; then >&2 echo "StartOS requires migrations: update-overlay is unavailable." && false; fi
$(call ssh,"sudo systemctl stop startd")
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
$(call ssh,"sudo systemctl start startd")
@@ -257,7 +266,7 @@ emulate-reflash: $(STARTOS_TARGETS)
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
upload-ota: results/$(BASENAME).squashfs
TARGET=$(TARGET) KEY=$(KEY) ./build/upload-ota.sh
TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh
container-runtime/debian.$(ARCH).squashfs: ./container-runtime/download-base-image.sh
ARCH=$(ARCH) ./container-runtime/download-base-image.sh
@@ -270,16 +279,16 @@ container-runtime/node_modules/.package-lock.json: container-runtime/package-loc
npm --prefix container-runtime ci
touch container-runtime/node_modules/.package-lock.json
ts-bindings: core/bindings/index.ts
ts-bindings: core/startos/bindings/index.ts
mkdir -p sdk/base/lib/osBindings
rsync -ac --delete core/bindings/ sdk/base/lib/osBindings/
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
core/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/bindings
./core/build/build-ts.sh
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/bindings/*.ts
touch core/bindings/index.ts
core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/startos/bindings
./core/build-ts.sh
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts
touch core/startos/bindings/index.ts
sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
(cd sdk && make bundle)
@@ -294,21 +303,21 @@ container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/pa
./container-runtime/install-dist-deps.sh
touch container-runtime/dist/node_modules/.package-lock.json
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/update-image-local.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
ARCH=$(ARCH) ./container-runtime/update-image-local.sh
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
ARCH=$(ARCH) REQUIRES=qemu ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh
build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(shell ls build/dpkg-deps/*)
PLATFORM=$(PLATFORM) ARCH=$(ARCH) build/dpkg-deps/generate.sh
$(FIRMWARE_ROMS): build/lib/firmware.json ./build/download-firmware.sh $(PLATFORM_FILE)
./build/download-firmware.sh $(PLATFORM)
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
./download-firmware.sh $(PLATFORM)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-startbox.sh
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-startbox.sh
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container: $(CORE_SRC) $(ENVIRONMENT_FILE)
ARCH=$(ARCH) ./core/build/build-start-container.sh
ARCH=$(ARCH) ./core/build-start-container.sh
touch core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
web/package-lock.json: web/package.json sdk/baseDist/package.json
@@ -324,27 +333,27 @@ web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json w
mkdir -p web/.angular
touch web/.angular/.updated
web/.i18n-checked: $(WEB_SHARED_SRC) $(WEB_UI_SRC) $(WEB_SETUP_WIZARD_SRC) $(WEB_START_TUNNEL_SRC)
npm --prefix web run check:i18n
touch web/.i18n-checked
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:ui
touch web/dist/raw/ui/index.html
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:setup
touch web/dist/raw/setup-wizard/index.html
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:install
touch web/dist/raw/install-wizard/index.html
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:tunnel
touch web/dist/raw/start-tunnel/index.html
web/dist/static/%/index.html: web/dist/raw/%/index.html
./web/compress-uis.sh $*
./compress-uis.sh $*
web/config.json: $(GIT_HASH_FILE) $(ENVIRONMENT_FILE) web/config-sample.json web/update-config.sh
./web/update-config.sh
web/config.json: $(GIT_HASH_FILE) web/config-sample.json
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json
npm --prefix patch-db/client ci
@@ -365,17 +374,17 @@ uis: $(WEB_UIS)
# this is a convenience step to build the UI
ui: web/dist/raw/ui
target/aarch64-unknown-linux-musl/release/pi-beep: ./build/build-cargo-dep.sh
ARCH=aarch64 ./build/build-cargo-dep.sh pi-beep
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep: ./build-cargo-dep.sh
ARCH=aarch64 ./build-cargo-dep.sh pi-beep
target/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console: ./build/build-cargo-dep.sh
ARCH=$(ARCH) ./build/build-cargo-dep.sh tokio-console
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console: ./build-cargo-dep.sh
ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console
touch $@
target/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs: ./build/build-cargo-dep.sh
ARCH=$(ARCH) ./build/build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs: ./build-cargo-dep.sh
ARCH=$(ARCH) ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
touch $@
target/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph: ./build/build-cargo-dep.sh
ARCH=$(ARCH) ./build/build-cargo-dep.sh flamegraph
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph: ./build-cargo-dep.sh
ARCH=$(ARCH) ./build-cargo-dep.sh flamegraph
touch $@

95
START-TUNNEL.md Normal file
View File

@@ -0,0 +1,95 @@
# StartTunnel
A self-hosted WireGuard VPN optimized for creating VLANs and reverse tunneling to personal servers.
You can think of StartTunnel as "virtual router in the cloud".
Use it for private remote access to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address.
## Features
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique WireGuard config that must be copied, downloaded, or scanned into the device.
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
## Installation
1. Rent a low cost VPS. For most use cases, the cheapest option should be enough.
- It must have a dedicated public IP address.
- For compute (CPU), memory (RAM), and storage (disk), choose the minimum spec.
- For transfer (bandwidth), it depends on (1) your use case and (2) your home Internet's _upload_ speed. Even if you intend to serve large files or stream content from your server, there is no reason to pay for speeds that exceed your home Internet's upload speed.
1. Provision the VPS with the latest version of Debian.
1. Access the VPS via SSH.
1. Run the StartTunnel install script:
curl -fsSL https://start9labs.github.io/start-tunnel | sh
1. [Initialize the web interface](#web-interface) (recommended)
## Updating
Simply re-run the install command:
```sh
curl -fsSL https://start9labs.github.io/start-tunnel | sh
```
## CLI
By default, StartTunnel is managed via the `start-tunnel` command line interface, which is self-documented.
```
start-tunnel --help
```
## Web Interface
Enable the web interface (recommended in most cases) to access your StartTunnel from the browser or via API.
1. Initialize the web interface.
start-tunnel web init
1. If your VPS has multiple public IP addresses, you will be prompted to select the IP address at which to host the web interface.
1. When prompted, enter the port at which to host the web interface. The default is 8443, and we recommend using it. If you change the default, choose an uncommon port to avoid future conflicts.
1. To access your StartTunnel web interface securely over HTTPS, you need an SSL certificate. When prompted, select whether to autogenerate a certificate or provide your own. _This is only for accessing your StartTunnel web interface_.
1. You will receive a success message with 3 pieces of information:
- **<https://IP:port>**: the URL where you can reach your personal web interface.
- **Password**: an autogenerated password for your interface. If you lose/forget it, you can reset it using the start-tunnel CLI.
- **Root Certificate Authority**: the Root CA of your StartTunnel instance.
1. If you autogenerated your SSL certificate, visiting the `https://IP:port` URL in the browser will warn you that the website is insecure. This is expected. You have two options for getting past this warning:
- option 1 (recommended): [Trust your StartTunnel Root CA on your connecting device](#trusting-your-starttunnel-root-ca).
- Option 2: bypass the warning in the browser, creating a one-time security exception.
### Trusting your StartTunnel Root CA
1. Copy the contents of your Root CA (starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----).
2. Open a text editor:
- Linux: gedit, nano, or any editor
- Mac: TextEdit
- Windows: Notepad
3. Paste the contents of your Root CA.
4. Save the file with a `.crt` extension (e.g. `start-tunnel.crt`) (make sure it saves as plain text, not rich text).
5. Trust the Root CA on your client device(s):
- [Linux](https://staging.docs.start9.com/device-guides/linux/ca.html)
- [Mac](https://staging.docs.start9.com/device-guides/mac/ca.html)
- [Windows](https://staging.docs.start9.com/device-guides/windows/ca.html)
- [Android/Graphene](https://staging.docs.start9.com/device-guides/android/ca.html)
- [iOS](https://staging.docs.start9.com/device-guides/ios/ca.html)

View File

@@ -1,9 +0,0 @@
# AI Agent TODOs
Pending tasks for AI agents. Remove items when completed.
## Unreviewed CLAUDE.md Sections
- [ ] Architecture - Web (`/web`) - @MattDHill

View File

@@ -10,7 +10,7 @@ When bumping from version `X.Y.Z-alpha.N` to `X.Y.Z-alpha.N+1`, you need to upda
### 1. Core Rust Crate Version
**File: `core/Cargo.toml`**
**File: `core/startos/Cargo.toml`**
Update the version string (line ~18):
@@ -31,7 +31,7 @@ This will update the version in `Cargo.lock` automatically.
### 2. Create New Version Migration Module
**File: `core/src/version/vX_Y_Z_alpha_N+1.rs`**
**File: `core/startos/src/version/vX_Y_Z_alpha_N+1.rs`**
Create a new version file by copying the previous version and updating:
@@ -79,7 +79,7 @@ impl VersionT for Version {
### 3. Update Version Module Registry
**File: `core/src/version/mod.rs`**
**File: `core/startos/src/version/mod.rs`**
Make changes in **5 locations**:
@@ -176,9 +176,9 @@ This pattern helps you quickly find all the places that need updating in the nex
## Summary Checklist
- [ ] Update `core/Cargo.toml` version
- [ ] Create new `core/src/version/vX_Y_Z_alpha_N+1.rs` file
- [ ] Update `core/src/version/mod.rs` in 5 locations
- [ ] Update `core/startos/Cargo.toml` version
- [ ] Create new `core/startos/src/version/vX_Y_Z_alpha_N+1.rs` file
- [ ] Update `core/startos/src/version/mod.rs` in 5 locations
- [ ] Run `cargo check` to update `core/Cargo.lock`
- [ ] Update `sdk/package/lib/StartSdk.ts` OSVersion
- [ ] Update `web/package.json` and `web/package-lock.json` version

View File

@@ -1,249 +0,0 @@
# Utilities & Patterns
This document covers common utilities and patterns used throughout the StartOS codebase.
## Util Module (`core/src/util/`)
The `util` module contains reusable utilities. Key submodules:
| Module | Purpose |
|--------|---------|
| `actor/` | Actor pattern implementation for concurrent state management |
| `collections/` | Custom collection types |
| `crypto.rs` | Cryptographic utilities (encryption, hashing) |
| `future.rs` | Future/async utilities |
| `io.rs` | File I/O helpers (create_file, canonicalize, etc.) |
| `iter.rs` | Iterator extensions |
| `net.rs` | Network utilities |
| `rpc.rs` | RPC helpers |
| `rpc_client.rs` | RPC client utilities |
| `serde.rs` | Serialization helpers (Base64, display/fromstr, etc.) |
| `sync.rs` | Synchronization primitives (SyncMutex, etc.) |
## Command Invocation (`Invoke` trait)
The `Invoke` trait provides a clean way to run external commands with error handling:
```rust
use crate::util::Invoke;
// Simple invocation
tokio::process::Command::new("ls")
.arg("-la")
.invoke(ErrorKind::Filesystem)
.await?;
// With timeout
tokio::process::Command::new("slow-command")
.timeout(Some(Duration::from_secs(30)))
.invoke(ErrorKind::Timeout)
.await?;
// With input
let mut input = Cursor::new(b"input data");
tokio::process::Command::new("cat")
.input(Some(&mut input))
.invoke(ErrorKind::Filesystem)
.await?;
// Piped commands
tokio::process::Command::new("cat")
.arg("file.txt")
.pipe(&mut tokio::process::Command::new("grep").arg("pattern"))
.invoke(ErrorKind::Filesystem)
.await?;
```
## Guard Pattern
Guards ensure cleanup happens when they go out of scope.
### `GeneralGuard` / `GeneralBoxedGuard`
For arbitrary cleanup actions:
```rust
use crate::util::GeneralGuard;
let guard = GeneralGuard::new(|| {
println!("Cleanup runs on drop");
});
// Do work...
// Explicit drop with action
guard.drop();
// Or skip the action
// guard.drop_without_action();
```
### `FileLock`
File-based locking with automatic unlock:
```rust
use crate::util::FileLock;
let lock = FileLock::new("/path/to/lockfile", true).await?; // blocking=true
// Lock held until dropped or explicitly unlocked
lock.unlock().await?;
```
## Mount Guard Pattern (`core/src/disk/mount/guard.rs`)
RAII guards for filesystem mounts. Ensures filesystems are unmounted when guards are dropped.
### `MountGuard`
Basic mount guard:
```rust
use crate::disk::mount::guard::MountGuard;
use crate::disk::mount::filesystem::{MountType, ReadOnly};
let guard = MountGuard::mount(&filesystem, "/mnt/target", ReadOnly).await?;
// Use the mounted filesystem at guard.path()
do_something(guard.path()).await?;
// Explicit unmount (or auto-unmounts on drop)
guard.unmount(false).await?; // false = don't delete mountpoint
```
### `TmpMountGuard`
Reference-counted temporary mount (mounts to `/media/startos/tmp/`):
```rust
use crate::disk::mount::guard::TmpMountGuard;
use crate::disk::mount::filesystem::ReadOnly;
// Multiple clones share the same mount
let guard1 = TmpMountGuard::mount(&filesystem, ReadOnly).await?;
let guard2 = guard1.clone();
// Mount stays alive while any guard exists
// Auto-unmounts when last guard is dropped
```
### `GenericMountGuard` trait
All mount guards implement this trait:
```rust
pub trait GenericMountGuard: std::fmt::Debug + Send + Sync + 'static {
fn path(&self) -> &Path;
fn unmount(self) -> impl Future<Output = Result<(), Error>> + Send;
}
```
### `SubPath`
Wraps a mount guard to point to a subdirectory:
```rust
use crate::disk::mount::guard::SubPath;
let mount = TmpMountGuard::mount(&filesystem, ReadOnly).await?;
let subdir = SubPath::new(mount, "data/subdir");
// subdir.path() returns the full path including subdirectory
```
## FileSystem Implementations (`core/src/disk/mount/filesystem/`)
Various filesystem types that can be mounted:
| Type | Description |
|------|-------------|
| `bind.rs` | Bind mounts |
| `block_dev.rs` | Block device mounts |
| `cifs.rs` | CIFS/SMB network shares |
| `ecryptfs.rs` | Encrypted filesystem |
| `efivarfs.rs` | EFI variables |
| `httpdirfs.rs` | HTTP directory as filesystem |
| `idmapped.rs` | ID-mapped mounts |
| `label.rs` | Mount by label |
| `loop_dev.rs` | Loop device mounts |
| `overlayfs.rs` | Overlay filesystem |
## Other Useful Utilities
### `Apply` / `ApplyRef` traits
Fluent method chaining:
```rust
use crate::util::Apply;
let result = some_value
.apply(|v| transform(v))
.apply(|v| another_transform(v));
```
### `Container<T>`
Async-safe optional container:
```rust
use crate::util::Container;
let container = Container::new(None);
container.set(value).await;
let taken = container.take().await;
```
### `HashWriter<H, W>`
Write data while computing hash:
```rust
use crate::util::HashWriter;
use sha2::Sha256;
let writer = HashWriter::new(Sha256::new(), file);
// Write data...
let (hasher, file) = writer.finish();
let hash = hasher.finalize();
```
### `Never` type
Uninhabited type for impossible cases:
```rust
use crate::util::Never;
fn impossible() -> Never {
// This function can never return
}
let never: Never = impossible();
never.absurd::<String>() // Can convert to any type
```
### `MaybeOwned<'a, T>`
Either borrowed or owned data:
```rust
use crate::util::MaybeOwned;
fn accept_either(data: MaybeOwned<'_, String>) {
// Use &*data to access the value
}
accept_either(MaybeOwned::from(&existing_string));
accept_either(MaybeOwned::from(owned_string));
```
### `new_guid()`
Generate a random GUID:
```rust
use crate::util::new_guid;
let guid = new_guid(); // Returns InternedString
```

View File

@@ -1,100 +0,0 @@
# i18n Patterns in `core/`
## Library & Setup
**Crate:** [`rust-i18n`](https://crates.io/crates/rust-i18n) v3.1.5 (`core/Cargo.toml`)
**Initialization** (`core/src/lib.rs:3`):
```rust
rust_i18n::i18n!("locales", fallback = ["en_US"]);
```
This macro scans `core/locales/` at compile time and embeds all translations as constants.
**Prelude re-export** (`core/src/prelude.rs:4`):
```rust
pub use rust_i18n::t;
```
Most modules import `t!` via the prelude.
## Translation File
**Location:** `core/locales/i18n.yaml`
**Format:** YAML v2 (~755 keys)
**Supported languages:** `en_US`, `de_DE`, `es_ES`, `fr_FR`, `pl_PL`
**Entry structure:**
```yaml
namespace.sub.key-name:
en_US: "English text with %{param}"
de_DE: "German text with %{param}"
# ...
```
## Using `t!()`
```rust
// Simple key
t!("error.unknown")
// With parameter interpolation (%{name} in YAML)
t!("bins.deprecated.renamed", old = old_name, new = new_name)
```
## Key Naming Conventions
Keys use **dot-separated hierarchical namespaces** with **kebab-case** for multi-word segments:
```
<module>.<submodule>.<descriptive-name>
```
Examples:
- `error.incorrect-password` — error kind label
- `bins.start-init.updating-firmware` — startup phase message
- `backup.bulk.complete-title` — backup notification title
- `help.arg.acme-contact` — CLI help text for an argument
- `context.diagnostic.starting-diagnostic-ui` — diagnostic context status
### Top-Level Namespaces
| Namespace | Purpose |
|-----------|---------|
| `error.*` | `ErrorKind` display strings (see `src/error.rs`) |
| `bins.*` | CLI binary messages (deprecated, start-init, startd, etc.) |
| `init.*` | Initialization phase labels |
| `setup.*` | First-run setup messages |
| `context.*` | Context startup messages (diagnostic, setup, CLI) |
| `service.*` | Service lifecycle messages |
| `backup.*` | Backup/restore operation messages |
| `registry.*` | Package registry messages |
| `net.*` | Network-related messages |
| `middleware.*` | Request middleware messages (auth, etc.) |
| `disk.*` | Disk operation messages |
| `lxc.*` | Container management messages |
| `system.*` | System monitoring/metrics messages |
| `notifications.*` | User-facing notification messages |
| `update.*` | OS update messages |
| `util.*` | Utility messages (TUI, RPC) |
| `ssh.*` | SSH operation messages |
| `shutdown.*` | Shutdown-related messages |
| `logs.*` | Log-related messages |
| `auth.*` | Authentication messages |
| `help.*` | CLI help text (`help.arg.<arg-name>`) |
| `about.*` | CLI command descriptions |
## Locale Selection
`core/src/bins/mod.rs:15-36``set_locale_from_env()`:
1. Reads `LANG` environment variable
2. Strips `.UTF-8` suffix
3. Exact-matches against available locales, falls back to language-prefix match (e.g. `en_GB` matches `en_US`)
## Adding New Keys
1. Add the key to `core/locales/i18n.yaml` with all 5 language translations
2. Use the `t!("your.key.name")` macro in Rust code
3. Follow existing namespace conventions — match the module path where the key is used
4. Use kebab-case for multi-word segments
5. Translations are validated at compile time

View File

@@ -1,226 +0,0 @@
# rpc-toolkit
StartOS uses [rpc-toolkit](https://github.com/Start9Labs/rpc-toolkit) for its JSON-RPC API. This document covers the patterns used in this codebase.
## Overview
The API is JSON-RPC (not REST). All endpoints are RPC methods organized in a hierarchical command structure.
## Handler Functions
There are four types of handler functions, chosen based on the function's characteristics:
### `from_fn_async` - Async handlers
For standard async functions. Most handlers use this.
```rust
pub async fn my_handler(ctx: RpcContext, params: MyParams) -> Result<MyResponse, Error> {
// Can use .await
}
from_fn_async(my_handler)
```
### `from_fn_async_local` - Non-thread-safe async handlers
For async functions that are not `Send` (cannot be safely moved between threads). Use when working with non-thread-safe types.
```rust
pub async fn cli_download(ctx: CliContext, params: Params) -> Result<(), Error> {
// Non-Send async operations
}
from_fn_async_local(cli_download)
```
### `from_fn_blocking` - Sync blocking handlers
For synchronous functions that perform blocking I/O or long computations.
```rust
pub fn query_dns(ctx: RpcContext, params: DnsParams) -> Result<DnsResponse, Error> {
// Blocking operations (file I/O, DNS lookup, etc.)
}
from_fn_blocking(query_dns)
```
### `from_fn` - Sync non-blocking handlers
For pure functions or quick synchronous operations with no I/O.
```rust
pub fn echo(ctx: RpcContext, params: EchoParams) -> Result<String, Error> {
Ok(params.message)
}
from_fn(echo)
```
## ParentHandler
Groups related RPC methods into a hierarchy:
```rust
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
pub fn my_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("list", from_fn_async(list_handler).with_call_remote::<CliContext>())
.subcommand("create", from_fn_async(create_handler).with_call_remote::<CliContext>())
}
```
## Handler Extensions
Chain methods to configure handler behavior.
**Ordering rules:**
1. `with_about()` must come AFTER other CLI modifiers (`no_display()`, `with_custom_display_fn()`, etc.)
2. `with_call_remote()` must be the LAST adapter in the chain
| Method | Purpose |
|--------|---------|
| `.with_metadata("key", Value)` | Attach metadata for middleware |
| `.no_cli()` | RPC-only, not available via CLI |
| `.no_display()` | No CLI output |
| `.with_display_serializable()` | Default JSON/YAML output for CLI |
| `.with_custom_display_fn(\|_, res\| ...)` | Custom CLI output formatting |
| `.with_about("about.description")` | Add help text (i18n key) - **after CLI modifiers** |
| `.with_call_remote::<CliContext>()` | Enable CLI to call remotely - **must be last** |
### Correct ordering example:
```rust
from_fn_async(my_handler)
.with_metadata("sync_db", Value::Bool(true)) // metadata early
.no_display() // CLI modifier
.with_about("about.my-handler") // after CLI modifiers
.with_call_remote::<CliContext>() // always last
```
## Metadata by Middleware
Metadata tags are processed by different middleware. Group them logically:
### Auth Middleware (`middleware/auth/mod.rs`)
| Metadata | Default | Description |
|----------|---------|-------------|
| `authenticated` | `true` | Whether endpoint requires authentication. Set to `false` for public endpoints. |
### Session Auth Middleware (`middleware/auth/session.rs`)
| Metadata | Default | Description |
|----------|---------|-------------|
| `login` | `false` | Special handling for login endpoints (rate limiting, cookie setting) |
| `get_session` | `false` | Inject session ID into params as `__Auth_session` |
### Signature Auth Middleware (`middleware/auth/signature.rs`)
| Metadata | Default | Description |
|----------|---------|-------------|
| `get_signer` | `false` | Inject signer public key into params as `__Auth_signer` |
### Registry Auth (extends Signature Auth)
| Metadata | Default | Description |
|----------|---------|-------------|
| `admin` | `false` | Require admin privileges (signer must be in admin list) |
| `get_device_info` | `false` | Inject device info header for hardware filtering |
### Database Middleware (`middleware/db.rs`)
| Metadata | Default | Description |
|----------|---------|-------------|
| `sync_db` | `false` | Sync database after mutation, add `X-Patch-Sequence` header |
## Context Types
Different contexts for different execution environments:
- `RpcContext` - Web/RPC requests with full service access
- `CliContext` - CLI operations, calls remote RPC
- `InitContext` - During system initialization
- `DiagnosticContext` - Diagnostic/recovery mode
- `RegistryContext` - Registry daemon context
- `EffectContext` - Service effects context (container-to-host calls)
## Parameter Structs
Parameters use derive macros for JSON-RPC, CLI parsing, and TypeScript generation:
```rust
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] // JSON-RPC uses camelCase
#[command(rename_all = "kebab-case")] // CLI uses kebab-case
#[ts(export)] // Generate TypeScript types
pub struct MyParams {
pub package_id: PackageId,
}
```
### Middleware Injection
Auth middleware can inject values into params using special field names:
```rust
#[derive(Deserialize, Serialize, Parser, TS)]
pub struct MyParams {
#[ts(skip)]
#[serde(rename = "__Auth_session")] // Injected by session auth
session: InternedString,
#[ts(skip)]
#[serde(rename = "__Auth_signer")] // Injected by signature auth
signer: AnyVerifyingKey,
#[ts(skip)]
#[serde(rename = "__Auth_userAgent")] // Injected during login
user_agent: Option<String>,
}
```
## Common Patterns
### Adding a New RPC Endpoint
1. Define params struct with `Deserialize, Serialize, Parser, TS`
2. Choose handler type based on sync/async and thread-safety
3. Write handler function taking `(Context, Params) -> Result<Response, Error>`
4. Add to parent handler with appropriate extensions (display modifiers before `with_about`)
5. TypeScript types auto-generated via `make ts-bindings`
### Public (Unauthenticated) Endpoint
```rust
from_fn_async(get_info)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("about.get-info")
.with_call_remote::<CliContext>() // last
```
### Mutating Endpoint with DB Sync
```rust
from_fn_async(update_config)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("about.update-config")
.with_call_remote::<CliContext>() // last
```
### Session-Aware Endpoint
```rust
from_fn_async(logout)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("about.logout")
.with_call_remote::<CliContext>() // last
```
## File Locations
- Handler definitions: Throughout `core/src/` modules
- Main API tree: `core/src/lib.rs` (`main_api()`, `server()`, `package()`)
- Auth middleware: `core/src/middleware/auth/`
- DB middleware: `core/src/middleware/db.rs`
- Context types: `core/src/context/`

View File

@@ -1,122 +0,0 @@
# S9PK Package Format
S9PK is the package format for StartOS services. Version 2 uses a merkle archive structure for efficient downloading and cryptographic verification.
## File Format
S9PK files begin with a 3-byte header: `0x3b 0x3b 0x02` (magic bytes + version 2).
The archive is cryptographically signed using Ed25519 with prehashed content (SHA-512 over blake3 merkle root hash).
## Archive Structure
```
/
├── manifest.json # Package metadata (required)
├── icon.<ext> # Package icon - any image/* format (required)
├── LICENSE.md # License text (required)
├── dependencies/ # Dependency metadata (optional)
│ └── <package-id>/
│ ├── metadata.json # DependencyMetadata
│ └── icon.<ext> # Dependency icon
├── javascript.squashfs # Package JavaScript code (required)
├── assets.squashfs # Static assets (optional, legacy: assets/ directory)
└── images/ # Container images by architecture
└── <arch>/ # e.g., x86_64, aarch64, riscv64
├── <image-id>.squashfs # Container filesystem
├── <image-id>.json # Image metadata
└── <image-id>.env # Environment variables
```
## Components
### manifest.json
The package manifest contains all metadata:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Package identifier (e.g., `bitcoind`) |
| `title` | string | Display name |
| `version` | string | Extended version string |
| `satisfies` | string[] | Version ranges this version satisfies |
| `releaseNotes` | string/object | Release notes (localized) |
| `canMigrateTo` | string | Version range for forward migration |
| `canMigrateFrom` | string | Version range for backward migration |
| `license` | string | License type |
| `wrapperRepo` | string | StartOS wrapper repository URL |
| `upstreamRepo` | string | Upstream project URL |
| `supportSite` | string | Support site URL |
| `marketingSite` | string | Marketing site URL |
| `donationUrl` | string? | Optional donation URL |
| `docsUrl` | string? | Optional documentation URL |
| `description` | object | Short and long descriptions (localized) |
| `images` | object | Image configurations by image ID |
| `volumes` | string[] | Volume IDs for persistent data |
| `alerts` | object | User alerts for lifecycle events |
| `dependencies` | object | Package dependencies |
| `hardwareRequirements` | object | Hardware requirements (arch, RAM, devices) |
| `hardwareAcceleration` | boolean | Whether package uses hardware acceleration |
| `gitHash` | string? | Git commit hash |
| `osVersion` | string | Minimum StartOS version |
| `sdkVersion` | string? | SDK version used to build |
### javascript.squashfs
Contains the package JavaScript that implements the `ABI` interface from `@start9labs/start-sdk-base`. This code runs in the container runtime and manages the package lifecycle.
The squashfs is mounted at `/usr/lib/startos/package/` and the runtime loads `index.js`.
### images/
Container images organized by architecture:
- **`<image-id>.squashfs`** - Container root filesystem
- **`<image-id>.json`** - Image metadata (entrypoint, user, workdir, etc.)
- **`<image-id>.env`** - Environment variables for the container
Images are built from Docker/Podman and converted to squashfs. The `ImageConfig` in manifest specifies:
- `arch` - Supported architectures
- `emulateMissingAs` - Fallback architecture for emulation
- `nvidiaContainer` - Whether to enable NVIDIA container support
### assets.squashfs
Static assets accessible to the package, mounted read-only at `/media/startos/assets/` in the container.
### dependencies/
Metadata for dependencies displayed in the UI:
- `metadata.json` - Just title for now
- `icon.<ext>` - Icon for the dependency
## Merkle Archive
The S9PK uses a merkle tree structure where each file and directory has a blake3 hash. This enables:
1. **Partial downloads** - Download and verify individual files
2. **Integrity verification** - Verify any subset of the archive
3. **Efficient updates** - Only download changed portions
4. **DOS protection** - Size limits enforced before downloading content
Files are sorted by priority for streaming (manifest first, then icon, license, dependencies, javascript, assets, images).
## Building S9PK
Use `start-cli s9pk pack` to build packages:
```bash
start-cli s9pk pack <manifest-path> -o <output.s9pk>
```
Images can be sourced from:
- Docker/Podman build (`--docker-build`)
- Existing Docker tag (`--docker-tag`)
- Pre-built squashfs files
## Related Code
- `core/src/s9pk/v2/mod.rs` - S9pk struct and serialization
- `core/src/s9pk/v2/manifest.rs` - Manifest types
- `core/src/s9pk/v2/pack.rs` - Packing logic
- `core/src/s9pk/merkle_archive/` - Merkle archive implementation

29
build-cargo-dep.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -e
shopt -s expand_aliases
if [ "$0" != "./build-cargo-dep.sh" ]; then
>&2 echo "Must be run from start-os directory"
exit 1
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
mkdir -p cargo-deps
source core/builder-alias.sh
RUSTFLAGS="-C target-feature=+crt-static"
rust-zig-builder cargo-zigbuild install $* --target-dir /workdir/cargo-deps/ --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd "cargo-deps/$RUST_ARCH-unknown-linux-musl/release/${!#}" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID cargo-deps && chown -R $UID:$UID /usr/local/cargo"
fi

View File

View File

@@ -1,26 +0,0 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")/.."
set -e
shopt -s expand_aliases
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
mkdir -p target
source core/build/builder-alias.sh
RUSTFLAGS="-C target-feature=+crt-static"
rust-zig-builder cargo-zigbuild install $* --target-dir /workdir/target/ --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd "target/$RUST_ARCH-unknown-linux-musl/release/${!#}" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID target && chown -R $UID:$UID /usr/local/cargo"
fi

View File

@@ -3,7 +3,6 @@ avahi-utils
b3sum
bash-completion
beep
binfmt-support
bmon
btrfs-progs
ca-certificates
@@ -16,7 +15,6 @@ dnsutils
dosfstools
e2fsprogs
ecryptfs-utils
equivs
exfatprogs
flashrom
fuse3
@@ -46,7 +44,6 @@ openssh-server
podman
psmisc
qemu-guest-agent
qemu-user-static
rfkill
rsync
samba-common-bin

View File

@@ -9,9 +9,6 @@ FEATURES+=("${ARCH}")
if [ "$ARCH" != "$PLATFORM" ]; then
FEATURES+=("${PLATFORM}")
fi
if [[ "$PLATFORM" =~ -nonfree$ ]]; then
FEATURES+=("nonfree")
fi
feature_file_checker='
/^#/ { next }

View File

@@ -1,10 +0,0 @@
+ firmware-amd-graphics
+ firmware-atheros
+ firmware-brcm80211
+ firmware-iwlwifi
+ firmware-libertas
+ firmware-misc-nonfree
+ firmware-realtek
+ nvidia-container-toolkit
# + nvidia-driver
# + nvidia-kernel-dkms

View File

@@ -1,35 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
BASEDIR="$(pwd -P)"
SUITE=trixie
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
dockerfile_hash=$(sha256sum ${BASEDIR}/build/image-recipe/Dockerfile | head -c 7)
docker_img_name="start9/build-iso:${SUITE}-${dockerfile_hash}"
platform=linux/${ARCH}
case $ARCH in
x86_64)
platform=linux/amd64;;
aarch64)
platform=linux/arm64;;
esac
if ! docker run --rm --platform=$platform "${docker_img_name}" true 2> /dev/null; then
docker buildx build --load --platform=$platform --build-arg=SUITE=${SUITE} -t "${docker_img_name}" ./build/image-recipe
fi
docker run $USE_TTY --rm --platform=$platform --privileged -v "$(pwd)/build/image-recipe:/root/image-recipe" -v "$(pwd)/results:/root/results" \
-e IB_SUITE="$SUITE" \
-e IB_UID="$UID" \
-e IB_INCLUDE \
"${docker_img_name}" /root/image-recipe/build.sh $@

View File

@@ -1,51 +0,0 @@
desktop-image: "../splash.png"
title-color: "#ffffff"
title-font: "Unifont Regular 16"
title-text: "StartOS Boot Menu with GRUB"
message-font: "Unifont Regular 16"
terminal-font: "Unifont Regular 16"
#help bar at the bottom
+ label {
top = 100%-50
left = 0
width = 100%
height = 20
text = "@KEYMAP_SHORT@"
align = "center"
color = "#ffffff"
font = "Unifont Regular 16"
}
#boot menu
+ boot_menu {
left = 10%
width = 80%
top = 52%
height = 48%-80
item_color = "#a8a8a8"
item_font = "Unifont Regular 16"
selected_item_color= "#ffffff"
selected_item_font = "Unifont Regular 16"
item_height = 16
item_padding = 0
item_spacing = 4
icon_width = 0
icon_heigh = 0
item_icon_space = 0
}
#progress bar
+ progress_bar {
id = "__timeout__"
left = 15%
top = 100%-80
height = 16
width = 70%
font = "Unifont Regular 16"
text_color = "#000000"
fg_color = "#ffffff"
bg_color = "#a8a8a8"
border_color = "#ffffff"
text = "@TIMEOUT_NOTIFICATION_LONG@"
}

View File

@@ -4,7 +4,7 @@ parse_essential_db_info() {
DB_DUMP="/tmp/startos_db.json"
if command -v start-cli >/dev/null 2>&1; then
timeout 30 start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
else
return 1
fi

View File

@@ -63,7 +63,7 @@ mount --bind /proc /media/startos/next/proc
mount --bind /boot /media/startos/next/boot
mount --bind /media/startos/root /media/startos/next/media/startos/root
if mountpoint /sys/firmware/efi/efivars 2>&1 > /dev/null; then
if mountpoint /sys/firmware/efi/efivars 2> /dev/null; then
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
fi
@@ -75,7 +75,7 @@ else
CHROOT_RES=$?
fi
if mountpoint /media/startos/next/sys/firmware/efi/efivars 2>&1 > /dev/null; then
if mountpoint /media/startos/next/sys/firmware/efi/efivars 2> /dev/null; then
umount /media/startos/next/sys/firmware/efi/efivars
fi
@@ -111,6 +111,6 @@ if [ "$CHROOT_RES" -eq 0 ]; then
reboot
fi
umount /media/startos/next
umount -R /media/startos/next
umount /media/startos/upper
rm -rf /media/startos/upper /media/startos/next

View File

@@ -35,20 +35,16 @@ if [ "$UNDO" = 1 ]; then
exit $err
fi
# DNAT: rewrite destination for incoming packets (external traffic)
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
# DNAT: rewrite destination for locally-originated packets (hairpin from host itself)
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
# MASQUERADE: rewrite source for all forwarded traffic to the destination
# This ensures responses are routed back through the host regardless of source IP
iptables -t nat -A ${NAME}_POSTROUTING -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
iptables -t nat -A ${NAME}_POSTROUTING -d "$dip" -p udp --dport "$dport" -j MASQUERADE
iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
# Allow new connections to be forwarded to the destination
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT

View File

@@ -1,20 +0,0 @@
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_NONINTERACTIVE_SEEN=true
TMP_DIR=$(mktemp -d)
(
set -e
cd $TMP_DIR
cat > control.equivs
equivs-build control.equivs
apt-get install -y ./*.deb < /dev/null
)
rm -rf $TMP_DIR
echo Install complete. >&2
exit 0

View File

@@ -29,13 +29,10 @@ if [ -z "$needed" ]; then
exit 1
fi
MARGIN=${MARGIN:-1073741824}
target=$((needed + MARGIN))
if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then
echo 'Pruning...'
current="$(readlink -f /media/startos/config/current.rootfs)"
while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$target" ]]; do
while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do
to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)"
if [ -e "$to_prune" ]; then
echo " Pruning $to_prune"

View File

@@ -50,12 +50,12 @@ mount --bind /proc /media/startos/next/proc
mount --bind /boot /media/startos/next/boot
mount --bind /media/startos/root /media/startos/next/media/startos/root
if mountpoint /boot/efi 2>&1 > /dev/null; then
if mountpoint /boot/efi 2> /dev/null; then
mkdir -p /media/startos/next/boot/efi
mount --bind /boot/efi /media/startos/next/boot/efi
fi
if mountpoint /sys/firmware/efi/efivars 2>&1 > /dev/null; then
if mountpoint /sys/firmware/efi/efivars 2> /dev/null; then
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
fi

View File

@@ -1,4 +1,4 @@
FROM debian:trixie
FROM debian:forky
RUN apt-get update && \
apt-get install -y \
@@ -12,14 +12,35 @@ RUN apt-get update && \
jq \
gzip \
brotli \
qemu-user-static \
binfmt-support \
squashfs-tools \
git \
debspawn \
rsync \
b3sum \
fuse-overlayfs \
sudo \
systemd \
systemd-container \
systemd-sysv \
dbus \
dbus-user-session \
nodejs
RUN systemctl mask \
systemd-firstboot.service \
systemd-udevd.service \
getty@tty1.service \
console-getty.service
RUN git config --global --add safe.directory /root/start-os
RUN mkdir -p /etc/debspawn && \
echo "AllowUnsafePermissions=true" > /etc/debspawn/global.toml
RUN mkdir -p /root/start-os
WORKDIR /root/start-os
COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT [ "/docker-entrypoint.sh" ]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec /lib/systemd/systemd --unit=multi-user.target --show-status=false --log-target=journal

View File

@@ -1,30 +1,27 @@
#!/bin/bash
pwd=$(pwd)
if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != "Linux" ] ) || ( [ "$REQUIRES" = "debian" ] && ! which dpkg > /dev/null ) || ( [ "$REQUIRES" = "qemu" ] && ! which qemu-$ARCH > /dev/null ); then
project_pwd="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)/"
pwd="$(pwd)/"
if ! [[ "$pwd" = "$project_pwd"* ]]; then
>&2 echo "Must be run from start-os project dir"
exit 1
fi
rel_pwd="${pwd#"$project_pwd"}"
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
set -e
rel_pwd="${pwd#"$(pwd)"}"
COMPAT_ARCH=$(uname -m)
platform=linux/$COMPAT_ARCH
case $COMPAT_ARCH in
x86_64)
platform=linux/amd64;;
aarch64)
platform=linux/arm64;;
esac
if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != "Linux" ] ) || ( [ "$REQUIRES" = "debian" ] && ! which dpkg > /dev/null ); then
SYSTEMD_TTY="-P"
USE_TTY=
if tty -s; then
USE_TTY="-it"
SYSTEMD_TTY="-t"
fi
docker run $USE_TTY --platform=$platform -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS -w "/root/start-os${rel_pwd}" --rm -v "$(pwd):/root/start-os" start9/build-env $@
docker run -d --rm --name os-compat --privileged --security-opt apparmor=unconfined -v "${project_pwd}:/root/start-os" -v /lib/modules:/lib/modules:ro start9/build-env
while ! docker exec os-compat systemctl is-active --quiet multi-user.target 2> /dev/null; do sleep .5; done
docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@
code=$?
docker stop os-compat > /dev/null
exit $code
else
exec $@
fi
fi

87
build/raspberrypi/make-image.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
set -e
function partition_for () {
if [[ "$1" =~ [0-9]+$ ]]; then
echo "$1p$2"
else
echo "$1$2"
fi
}
VERSION=$(cat VERSION.txt)
ENVIRONMENT=$(cat ENVIRONMENT.txt)
GIT_HASH=$(cat GIT_HASH.txt | head -c 7)
DATE=$(date +%Y%m%d)
ROOT_PART_END=7217792
VERSION_FULL="$VERSION-$GIT_HASH"
if [ -n "$ENVIRONMENT" ]; then
VERSION_FULL="$VERSION_FULL~$ENVIRONMENT"
fi
TARGET_NAME=startos-${VERSION_FULL}-${DATE}_raspberrypi.img
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
rm -f $TARGET_NAME
truncate -s $TARGET_SIZE $TARGET_NAME
(
echo o
echo x
echo i
echo "0xcb15ae4d"
echo r
echo n
echo p
echo 1
echo 2048
echo 526335
echo t
echo c
echo n
echo p
echo 2
echo 526336
echo $ROOT_PART_END
echo a
echo 1
echo w
) | fdisk $TARGET_NAME
OUTPUT_DEVICE=$(sudo losetup --show -fP $TARGET_NAME)
sudo mkfs.ext4 `partition_for ${OUTPUT_DEVICE} 2`
sudo mkfs.vfat `partition_for ${OUTPUT_DEVICE} 1`
TMPDIR=$(mktemp -d)
sudo mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR
sudo mkdir $TMPDIR/boot
sudo mount `partition_for ${OUTPUT_DEVICE} 1` $TMPDIR/boot
sudo unsquashfs -f -d $TMPDIR startos.raspberrypi.squashfs
REAL_GIT_HASH=$(cat $TMPDIR/usr/lib/startos/GIT_HASH.txt)
REAL_VERSION=$(cat $TMPDIR/usr/lib/startos/VERSION.txt)
REAL_ENVIRONMENT=$(cat $TMPDIR/usr/lib/startos/ENVIRONMENT.txt)
sudo sed -i 's| boot=startos| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
sudo cp ./build/raspberrypi/fstab $TMPDIR/etc/
sudo cp ./build/raspberrypi/init_resize.sh $TMPDIR/usr/lib/startos/scripts/init_resize.sh
sudo umount $TMPDIR/boot
sudo umount $TMPDIR
sudo losetup -d $OUTPUT_DEVICE
if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then
if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then
>&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch"
>&2 echo "expected $REAL_GIT_HASH (dpkg) found $(cat GIT_HASH.txt) (repo)"
exit 1
fi
if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then
>&2 echo "startos.raspberrypi.squashfs VERSION.txt mismatch"
exit 1
fi
if [ "$(cat ENVIRONMENT.txt)" != "$REAL_ENVIRONMENT" ]; then
>&2 echo "startos.raspberrypi.squashfs ENVIRONMENT.txt mismatch"
exit 1
fi
fi

View File

@@ -1,142 +0,0 @@
#!/bin/bash
if [ -z "$VERSION" ]; then
>&2 echo '$VERSION required'
exit 2
fi
set -e
if [ "$SKIP_DL" != "1" ]; then
if [ "$SKIP_CLEAN" != "1" ]; then
rm -rf ~/Downloads/v$VERSION
mkdir ~/Downloads/v$VERSION
cd ~/Downloads/v$VERSION
fi
if [ -n "$RUN_ID" ]; then
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
done
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
done
fi
if [ -n "$ST_RUN_ID" ]; then
for arch in aarch64 riscv64 x86_64; do
while ! gh run download -R Start9Labs/start-os $ST_RUN_ID -n start-tunnel_$arch.deb -D $(pwd); do sleep 1; done
done
fi
if [ -n "$CLI_RUN_ID" ]; then
for arch in aarch64 riscv64 x86_64; do
for os in linux macos; do
pair=${arch}-${os}
if [ "${pair}" = "riscv64-linux" ]; then
target=riscv64gc-unknown-linux-musl
elif [ "${pair}" = "riscv64-macos" ]; then
continue
elif [ "${os}" = "linux" ]; then
target="${arch}-unknown-linux-musl"
elif [ "${os}" = "macos" ]; then
target="${arch}-apple-darwin"
fi
while ! gh run download -R Start9Labs/start-os $CLI_RUN_ID -n start-cli_$target -D $(pwd); do sleep 1; done
mv start-cli "start-cli_${pair}"
done
done
fi
else
cd ~/Downloads/v$VERSION
fi
start-cli --registry=https://alpha-registry-x.start9.com registry os version add $VERSION "v$VERSION" '' ">=0.3.5 <=$VERSION"
if [ "$SKIP_UL" = "2" ]; then
exit 2
elif [ "$SKIP_UL" != "1" ]; then
for file in *.deb start-cli_*; do
gh release upload -R Start9Labs/start-os v$VERSION $file
done
for file in *.iso *.squashfs; do
s3cmd put -P $file s3://startos-images/v$VERSION/$file
done
fi
if [ "$SKIP_INDEX" != "1" ]; then
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
for file in *_$arch.squashfs *_$arch.iso; do
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$file
done
done
fi
for file in *.iso *.squashfs *.deb start-cli_*; do
gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file"
done
gpg --export -a 7CFFDA41CA66056A > dr-bonez.key.asc
tar -czvf signatures.tar.gz *.asc
gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz
cat << EOF
# ISO Downloads
- [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.iso))
- [x86_64/AMD64-slim (FOSS-only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers")
- [aarch64/ARM64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nonfree.iso))
- [aarch64/ARM64-slim (FOSS-Only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso))
EOF
cat << 'EOF'
# StartOS Checksums
## SHA-256
```
EOF
sha256sum *.iso *.squashfs
cat << 'EOF'
```
## BLAKE-3
```
EOF
b3sum *.iso *.squashfs
cat << 'EOF'
```
# Start-Tunnel Checksums
## SHA-256
```
EOF
sha256sum start-tunnel*.deb
cat << 'EOF'
```
## BLAKE-3
```
EOF
b3sum start-tunnel*.deb
cat << 'EOF'
```
# start-cli Checksums
## SHA-256
```
EOF
sha256sum start-cli_*
cat << 'EOF'
```
## BLAKE-3
```
EOF
b3sum start-cli_*
cat << 'EOF'
```
EOF

View File

@@ -1,10 +1,8 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
if ! [ -f ./ENVIRONMENT.txt ] || [ "$(cat ./ENVIRONMENT.txt)" != "$ENVIRONMENT" ]; then
>&2 echo "Updating ENVIRONMENT.txt to \"$ENVIRONMENT\""
echo -n "$ENVIRONMENT" > ./ENVIRONMENT.txt
fi
echo -n ./build/env/ENVIRONMENT.txt
echo -n ./ENVIRONMENT.txt

View File

@@ -1,7 +1,5 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then
GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)"
else
@@ -13,4 +11,4 @@ if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then
echo -n "$GIT_HASH" > ./GIT_HASH.txt
fi
echo -n ./build/env/GIT_HASH.txt
echo -n ./GIT_HASH.txt

View File

@@ -1,10 +1,8 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
if ! [ -f ./PLATFORM.txt ] || [ "$(cat ./PLATFORM.txt)" != "$PLATFORM" ] && [ -n "$PLATFORM" ]; then
>&2 echo "Updating PLATFORM.txt to \"$PLATFORM\""
echo -n "$PLATFORM" > ./PLATFORM.txt
fi
echo -n ./build/env/PLATFORM.txt
echo -n ./PLATFORM.txt

View File

@@ -1,8 +1,6 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
FE_VERSION="$(cat ../../web/package.json | grep '"version"' | sed 's/[ \t]*"version":[ \t]*"\([^"]*\)",/\1/')"
FE_VERSION="$(cat web/package.json | grep '"version"' | sed 's/[ \t]*"version":[ \t]*"\([^"]*\)",/\1/')"
# TODO: Validate other version sources - backend/Cargo.toml, backend/src/version/mod.rs
@@ -12,4 +10,4 @@ if ! [ -f ./VERSION.txt ] || [ "$(cat ./VERSION.txt)" != "$VERSION" ]; then
echo -n "$VERSION" > ./VERSION.txt
fi
echo -n ./build/env/VERSION.txt
echo -n ./VERSION.txt

View File

@@ -4,8 +4,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e
STATIC_DIR=dist/static/$1
RAW_DIR=dist/raw/$1
STATIC_DIR=web/dist/static/$1
RAW_DIR=web/dist/raw/$1
mkdir -p $STATIC_DIR
rm -rf $STATIC_DIR

View File

@@ -1,21 +1,16 @@
# Container RPC Server Specification
The container runtime exposes a JSON-RPC server over a Unix socket at `/media/startos/rpc/service.sock`.
# Container RPC SERVER Specification
## Methods
### init
Initialize the runtime and system.
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
#### params
called after os has mounted js and images to the container
```ts
{
id: string,
kind: "install" | "update" | "restore" | null,
}
```
#### args
`[]`
#### response
@@ -23,16 +18,11 @@ Initialize the runtime and system.
### exit
Shutdown runtime and optionally run exit hooks for a target version.
shutdown runtime
#### params
#### args
```ts
{
id: string,
target: string | null, // ExtendedVersion or VersionRange
}
```
`[]`
#### response
@@ -40,11 +30,11 @@ Shutdown runtime and optionally run exit hooks for a target version.
### start
Run main method if not already running.
run main method if not already running
#### params
#### args
None
`[]`
#### response
@@ -52,11 +42,11 @@ None
### stop
Stop main method by sending SIGTERM to child processes, and SIGKILL after timeout.
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
#### params
#### args
None
`{ timeout: millis }`
#### response
@@ -64,16 +54,15 @@ None
### execute
Run a specific package procedure.
run a specific package procedure
#### params
#### args
```ts
{
id: string, // event ID
procedure: string, // JSON path (e.g., "/backup/create", "/actions/{name}/run")
input: any,
timeout: number | null,
procedure: JsonPath,
input: any,
timeout: millis,
}
```
@@ -83,64 +72,18 @@ Run a specific package procedure.
### sandbox
Run a specific package procedure in sandbox mode. Same interface as `execute`.
run a specific package procedure in sandbox mode
UNIMPLEMENTED: this feature is planned but does not exist
#### params
#### args
```ts
{
id: string,
procedure: string,
input: any,
timeout: number | null,
procedure: JsonPath,
input: any,
timeout: millis,
}
```
#### response
`any`
### callback
Handle a callback from an effect.
#### params
```ts
{
id: number,
args: any[],
}
```
#### response
`null` (no response sent)
### eval
Evaluate a script in the runtime context. Used for debugging.
#### params
```ts
{
script: string,
}
```
#### response
`any`
## Procedures
The `execute` and `sandbox` methods route to procedures based on the `procedure` path:
| Procedure | Description |
|-----------|-------------|
| `/backup/create` | Create a backup |
| `/actions/{name}/getInput` | Get input spec for an action |
| `/actions/{name}/run` | Run an action with input |

View File

@@ -2,6 +2,9 @@
set -e
mkdir -p /run/systemd/resolve
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
apt-get update
apt-get install -y curl rsync qemu-user-static nodejs
@@ -13,4 +16,7 @@ sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.co
systemctl enable container-runtime.service
rm -rf /run/systemd
rm -f /etc/resolv.conf
echo "nameserver 10.0.3.1" > /etc/resolv.conf

View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -e
IMAGE=$1
if [ -z "$IMAGE" ]; then
>&2 echo "usage: $0 <image id>"
exit 1
fi
if ! [ -d "/media/images/$IMAGE" ]; then
>&2 echo "image does not exist"
exit 1
fi
container=$(mktemp -d)
mkdir -p $container/rootfs $container/upper $container/work
mount -t overlay -olowerdir=/media/images/$IMAGE,upperdir=$container/upper,workdir=$container/work overlay $container/rootfs
rootfs=$container/rootfs
for special in dev sys proc run; do
mkdir -p $rootfs/$special
mount --bind /$special $rootfs/$special
done
echo $rootfs

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.48",
"version": "0.4.0-beta.45",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
rootfs=$1
if [ -z "$rootfs" ]; then
>&2 echo "usage: $0 <container rootfs path>"
exit 1
fi
umount --recursive $rootfs
rm -rf $rootfs/..

View File

@@ -178,13 +178,6 @@ export function makeEffects(context: EffectContext): Effects {
T.Effects["getInstalledPackages"]
>
},
getServiceManifest(
...[options]: Parameters<T.Effects["getServiceManifest"]>
) {
return rpcRound("get-service-manifest", options) as ReturnType<
T.Effects["getServiceManifest"]
>
},
subcontainer: {
createFs(options: { imageId: string; name: string }) {
return rpcRound("subcontainer.create-fs", options) as ReturnType<
@@ -319,7 +312,6 @@ export function makeEffects(context: EffectContext): Effects {
}
if (context.callbacks?.onLeaveContext)
self.onLeaveContext(() => {
self.constRetry = undefined
self.isInContext = false
self.onLeaveContext = () => {
console.warn(

View File

@@ -10,6 +10,7 @@ import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
/**
* We wanted something to represent what the main loop is doing, and
* in this case it used to run the properties, health, and the docker/ js main.

View File

@@ -50,7 +50,6 @@ import {
transformOldConfigToNew,
} from "./transformConfigSpec"
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
import { Volume } from "@start9labs/start-sdk/package/lib/util/Volume"
type Optional<A> = A | undefined | null
function todo(): never {
@@ -62,14 +61,14 @@ export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
const configFile = FileHelper.json(
{
base: new Volume("embassy"),
volumeId: "embassy",
subpath: "config.json",
},
matches.any,
)
const dependsOnFile = FileHelper.json(
{
base: new Volume("embassy"),
volumeId: "embassy",
subpath: "dependsOn.json",
},
dictionary([string, array(string)]),
@@ -288,6 +287,7 @@ function convertProperties(
}
}
const DEFAULT_REGISTRY = "https://registry.start9.com"
export class SystemForEmbassy implements System {
private version: ExtendedVersion
currentRunning: MainLoop | undefined
@@ -331,10 +331,6 @@ export class SystemForEmbassy implements System {
) {
this.version.upstream.prerelease = ["alpha"]
}
if (this.manifest.id === "nostr") {
this.manifest.id = "nostr-rs-relay"
}
}
async init(

View File

@@ -1,21 +0,0 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")/.."
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
DOCKER_PLATFORM=linux/${ARCH}
case $ARCH in
x86_64)
DOCKER_PLATFORM=linux/amd64;;
aarch64)
DOCKER_PLATFORM=linux/arm64;;
esac
docker run --rm $USE_TTY --platform=$DOCKER_PLATFORM -eARCH --privileged -v "$(pwd):/root/start-os" start9/build-env /root/start-os/container-runtime/update-image.sh
if [ "$(ls -nd "rootfs.${ARCH}.squashfs" | awk '{ print $3 }')" != "$UID" ]; then
docker run --rm $USE_TTY -v "$(pwd):/root/start-os" start9/build-env chown -R $UID:$UID /root/start-os/container-runtime
fi

View File

@@ -9,34 +9,56 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
mount -t tmpfs tmpfs /tmp
mkdir -p /tmp/lower /tmp/upper /tmp/work /tmp/combined
mount -o loop debian.${ARCH}.squashfs /tmp/lower
mount -t overlay -olowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work overlay /tmp/combined
if mountpoint -q tmp/combined; then sudo umount -l tmp/combined; fi
if mountpoint -q tmp/lower; then sudo umount tmp/lower; fi
sudo rm -rf tmp
mkdir -p tmp/lower tmp/upper tmp/work tmp/combined
if which squashfuse > /dev/null; then
sudo squashfuse debian.${ARCH}.squashfs tmp/lower
else
sudo mount debian.${ARCH}.squashfs tmp/lower
fi
if which fuse-overlayfs > /dev/null; then
sudo fuse-overlayfs -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
else
sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
fi
mkdir -p /tmp/combined/usr/lib/startos/
rsync -a --copy-unsafe-links --info=progress2 dist/ /tmp/combined/usr/lib/startos/init/
chown -R 0:0 /tmp/combined/usr/lib/startos/
cp container-runtime.service /tmp/combined/lib/systemd/system/container-runtime.service
chown 0:0 /tmp/combined/lib/systemd/system/container-runtime.service
cp container-runtime-failure.service /tmp/combined/lib/systemd/system/container-runtime-failure.service
chown 0:0 /tmp/combined/lib/systemd/system/container-runtime-failure.service
cp ../core/target/${RUST_ARCH}-unknown-linux-musl/release/start-container /tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container "$@"' > /tmp/combined/usr/bin/start-cli # TODO: remove
chmod +x /tmp/combined/usr/bin/start-cli
chown 0:0 /tmp/combined/usr/bin/start-container
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) > /tmp/combined/etc/machine-id
rm -f /tmp/combined/etc/resolv.conf
cp /etc/resolv.conf /tmp/combined/etc/resolv.conf
for fs in proc sys dev; do
mount --bind /$fs /tmp/combined/$fs
done
cat deb-install.sh | chroot /tmp/combined /bin/bash
for fs in proc sys dev; do
umount /tmp/combined/$fs
done
truncate -s 0 /tmp/combined/etc/machine-id
QEMU=
if [ "$ARCH" != "$(uname -m)" ]; then
QEMU=/usr/bin/qemu-${ARCH}
if ! which qemu-$ARCH > /dev/null; then
>&2 echo qemu-user is required for cross-platform builds
sudo umount tmp/combined
sudo umount tmp/lower
sudo rm -rf tmp
exit 1
fi
sudo cp $(which qemu-$ARCH) tmp/combined${QEMU}
fi
sudo mkdir -p tmp/combined/usr/lib/startos/
sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/
sudo chown -R 0:0 tmp/combined/usr/lib/startos/
sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runtime.service
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service
sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo cp ../core/target/${RUST_ARCH}-unknown-linux-musl/release/start-container tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container "$@"' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
sudo chmod +x tmp/combined/usr/bin/start-cli
sudo chown 0:0 tmp/combined/usr/bin/start-container
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id
cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash
sudo truncate -s 0 tmp/combined/etc/machine-id
if [ -n "$QEMU" ]; then
sudo rm tmp/combined${QEMU}
fi
rm -f rootfs.${ARCH}.squashfs
mkdir -p ../build/lib/container-runtime
mksquashfs /tmp/combined rootfs.${ARCH}.squashfs
sudo mksquashfs tmp/combined rootfs.${ARCH}.squashfs
sudo umount tmp/combined
sudo umount tmp/lower
sudo rm -rf tmp

2
core/.gitignore vendored
View File

@@ -8,4 +8,4 @@ secrets.db
.env
.editorconfig
proptest-regressions/**/*
/bindings/*
/startos/bindings/*

1072
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,292 +1,3 @@
[package]
authors = ["Aiden McClelland <me@drbonez.dev>"]
description = "The core of StartOS"
documentation = "https://docs.rs/start-os"
edition = "2024"
keywords = [
"bitcoin",
"full-node",
"lightning",
"privacy",
"raspberry-pi",
"self-hosted",
]
license = "MIT"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.19" # VERSION_BUMP
[workspace]
[lib]
name = "startos"
path = "src/lib.rs"
[[bin]]
name = "startbox"
path = "src/main/startbox.rs"
[[bin]]
name = "start-cli"
path = "src/main/start-cli.rs"
[[bin]]
name = "start-container"
path = "src/main/start-container.rs"
[[bin]]
name = "registrybox"
path = "src/main/registrybox.rs"
[[bin]]
name = "tunnelbox"
path = "src/main/tunnelbox.rs"
[features]
arti = [
"arti-client",
"safelog",
"tor-cell",
"tor-hscrypto",
"tor-hsservice",
"tor-keymgr",
"tor-llcrypto",
"tor-proto",
"tor-rtcompat",
]
beta = []
console = ["console-subscriber", "tokio/tracing"]
default = []
dev = []
test = []
unstable = ["backtrace-on-stack-overflow"]
[dependencies]
aes = { version = "0.7.5", features = ["ctr"] }
arti-client = { version = "0.33", features = [
"compression",
"ephemeral-keystore",
"experimental-api",
"onion-service-client",
"onion-service-service",
"rustls",
"static",
"tokio",
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls",
"use_tokio",
] }
async-compression = { version = "0.4.32", features = [
"brotli",
"gzip",
"tokio",
"zstd",
] }
async-stream = "0.3.5"
async-trait = "0.1.74"
axum = { version = "0.8.4", features = ["ws", "http2"] }
backtrace-on-stack-overflow = { version = "0.3.0", optional = true }
base32 = "0.5.0"
base64 = "0.22.1"
base64ct = "1.6.0"
basic-cookies = "0.1.4"
blake3 = { version = "1.5.0", features = ["mmap", "rayon"] }
bytes = "1"
chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.12", features = ["string"] }
color-eyre = "0.6.2"
console = "0.16.2"
console-subscriber = { version = "0.5.0", optional = true }
const_format = "0.2.34"
cookie = "0.18.0"
cookie_store = "0.22.0"
curve25519-dalek = "4.1.3"
der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7"
divrem = "1.0.0"
dns-lookup = "3.0.1"
ed25519 = { version = "2.2.3", features = ["alloc", "pem", "pkcs8"] }
ed25519-dalek = { version = "2.2.0", features = [
"digest",
"hazmat",
"pkcs8",
"rand_core",
"serde",
"zeroize",
] }
ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" }
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
"serde",
] }
fd-lock-rs = "0.1.4"
form_urlencoded = "1.2.1"
futures = "0.3.28"
gpt = "4.1.0"
hex = "0.4.3"
hickory-server = { version = "0.25.2", features = ["resolver"] }
hmac = "0.12.1"
http = "1.0.0"
http-body-util = "0.1"
hyper = { version = "1.5", features = ["http1", "http2", "server"] }
hyper-util = { version = "0.1.10", features = [
"http1",
"http2",
"server",
"server-auto",
"server-graceful",
"service",
"tokio",
] }
id-pool = { version = "0.2.2", default-features = false, features = [
"serde",
"u16",
] }
iddqd = "0.3.14"
imbl = { version = "6", features = ["serde", "small-chunks"] }
imbl-value = { version = "0.4.3", features = ["ts-rs"] }
include_dir = { version = "0.7.3", features = ["metadata"] }
indexmap = { version = "2.0.2", features = ["serde"] }
indicatif = { version = "0.18.3", features = ["tokio"] }
inotify = "0.11.0"
integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
ipnet = { version = "2.8.0", features = ["serde"] }
isocountry = "0.3.2"
itertools = "0.14.0"
jaq-core = "0.10.1"
jaq-std = "0.10.0"
josekit = "0.10.3"
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
lazy_async_pool = "0.3.3"
lazy_format = "2.0"
lazy_static = "1.4.0"
lettre = { version = "0.11.18", default-features = false, features = [
"aws-lc-rs",
"builder",
"hostname",
"pool",
"rustls-platform-verifier",
"smtp-transport",
"tokio1-rustls",
] }
libc = "0.2.149"
log = "0.4.20"
mbrman = "0.6.0"
miette = { version = "7.6.0", features = ["fancy"] }
mio = "1"
new_mime_guess = "4"
nix = { version = "0.30.1", features = [
"fs",
"hostname",
"mount",
"net",
"process",
"sched",
"signal",
"user",
] }
nom = "8.0.0"
num = "0.4.1"
num_cpus = "1.16.0"
num_enum = "0.7.0"
once_cell = "1.19.0"
openssh-keys = "0.6.2"
openssl = { version = "0.10.57", features = ["vendored"] }
p256 = { version = "0.13.2", features = ["pem"] }
patch-db = { version = "*", path = "../patch-db/patch-db", features = [
"trace",
] }
pbkdf2 = "0.12.2"
pin-project = "1.1.3"
pkcs8 = { version = "0.10.2", features = ["std"] }
prettytable-rs = "0.10.0"
proptest = "1.3.1"
proptest-derive = "0.7.0"
qrcode = "0.14.1"
r3bl_tui = "0.7.6"
rand = "0.9.2"
regex = "1.10.2"
reqwest = { version = "0.12.25", features = [
"json",
"socks",
"stream",
"http2",
] }
reqwest_cookie_store = "0.9.0"
rpassword = "7.2.0"
rust-argon2 = "3.0.0"
rust-i18n = "3.1.5"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0"
serde_toml = { package = "toml", version = "0.9.9+spec-1.0.0" }
serde_yaml = { package = "serde_yml", version = "0.0.12" }
sha-crypt = "0.5.0"
sha2 = "0.10.2"
signal-hook = "0.3.17"
socket2 = { version = "0.6.0", features = ["all"] }
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
sqlx = { version = "0.8.6", features = [
"postgres",
"runtime-tokio-rustls",
], default-features = false }
sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40"
termion = "4.0.5"
textwrap = "0.16.1"
thiserror = "2.0.12"
tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.4"
tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] }
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] }
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hscrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-keymgr = { version = "0.33", features = [
"ephemeral-keystore",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-llcrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-rtcompat = { version = "0.33", features = [
"rustls",
"tokio",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
torut = "0.2.1"
tower-service = "0.3.3"
tracing = "0.1.39"
tracing-error = "0.2.0"
tracing-journald = "0.3.0"
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
ts-rs = "9.0.1"
typed-builder = "0.23.2"
url = { version = "2.4.1", features = ["serde"] }
uuid = { version = "1.4.1", features = ["v4"] }
visit-rs = "0.1.1"
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
zbus = "5.1.1"
hashing-serializer = "0.1.1"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18.0"
pty-process = "0.5.1"
[profile.test]
opt-level = 3
[profile.dev]
opt-level = 3
[profile.dev.package.backtrace]
opt-level = 3
[profile.dev.package.sqlx-macros]
opt-level = 3
members = ["startos"]

View File

@@ -26,7 +26,7 @@ PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug" ]; then
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
@@ -60,7 +60,7 @@ if [ -z "${TARGET:-}" ]; then
fi
fi
cd ../..
cd ..
FEATURES="$(echo "${ENVIRONMENT:-}" | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then

View File

@@ -30,7 +30,7 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ../..
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""

View File

@@ -30,7 +30,7 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ../..
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""

View File

@@ -30,7 +30,7 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ../..
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""

View File

@@ -30,7 +30,7 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ../..
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
@@ -39,6 +39,6 @@ fi
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-zig-builder cargo test --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features test,$FEATURES --locked 'export_bindings_'
if [ "$(ls -nd "core/bindings" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID core/bindings && chown -R $UID:$UID /usr/local/cargo"
if [ "$(ls -nd "core/startos/bindings" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID core/startos/bindings && chown -R $UID:$UID /usr/local/cargo"
fi

View File

@@ -30,7 +30,7 @@ if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ../..
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
cd "$(dirname "${BASH_SOURCE[0]}")"
source ./build/builder-alias.sh
source ./builder-alias.sh
set -ea
shopt -s expand_aliases

View File

@@ -1,11 +0,0 @@
use rust_i18n::t;
pub fn renamed(old: &str, new: &str) -> ! {
eprintln!("{}", t!("bins.deprecated.renamed", old = old, new = new));
std::process::exit(1)
}
pub fn removed(name: &str) -> ! {
eprintln!("{}", t!("bins.deprecated.removed", name = name));
std::process::exit(1)
}

View File

@@ -1,194 +0,0 @@
use std::collections::{BTreeMap, VecDeque};
use std::ffi::OsString;
use std::path::Path;
use rust_i18n::t;
pub mod container_cli;
pub mod deprecated;
pub mod registry;
pub mod start_cli;
pub mod start_init;
pub mod startd;
pub mod tunnel;
pub fn set_locale_from_env() {
let lang = std::env::var("LANG").ok();
let lang = lang
.as_deref()
.map_or("C", |l| l.strip_suffix(".UTF-8").unwrap_or(l));
set_locale(lang)
}
pub fn set_locale(lang: &str) {
let mut best = None;
let prefix = lang.split_inclusive("_").next().unwrap();
for l in rust_i18n::available_locales!() {
if l == lang {
best = Some(l);
break;
}
if best.is_none() && l.starts_with(prefix) {
best = Some(l);
}
}
rust_i18n::set_locale(best.unwrap_or(lang));
}
pub fn translate_cli(mut cmd: clap::Command) -> clap::Command {
fn translate(s: impl std::fmt::Display) -> String {
t!(s.to_string()).into_owned()
}
if let Some(s) = cmd.get_about() {
let s = translate(s);
cmd = cmd.about(s);
}
if let Some(s) = cmd.get_long_about() {
let s = translate(s);
cmd = cmd.long_about(s);
}
if let Some(s) = cmd.get_before_help() {
let s = translate(s);
cmd = cmd.before_help(s);
}
if let Some(s) = cmd.get_before_long_help() {
let s = translate(s);
cmd = cmd.before_long_help(s);
}
if let Some(s) = cmd.get_after_help() {
let s = translate(s);
cmd = cmd.after_help(s);
}
if let Some(s) = cmd.get_after_long_help() {
let s = translate(s);
cmd = cmd.after_long_help(s);
}
let arg_ids = cmd
.get_arguments()
.map(|a| a.get_id().clone())
.collect::<Vec<_>>();
for id in arg_ids {
cmd = cmd.mut_arg(id, |arg| {
let arg = if let Some(s) = arg.get_help() {
let s = translate(s);
arg.help(s)
} else {
arg
};
if let Some(s) = arg.get_long_help() {
let s = translate(s);
arg.long_help(s)
} else {
arg
}
});
}
for cmd in cmd.get_subcommands_mut() {
*cmd = translate_cli(cmd.clone());
}
cmd
}
#[derive(Default)]
pub struct MultiExecutable {
default: Option<&'static str>,
bins: BTreeMap<&'static str, fn(VecDeque<OsString>)>,
}
impl MultiExecutable {
pub fn enable_startd(&mut self) -> &mut Self {
self.bins.insert("startd", startd::main);
self.bins
.insert("embassyd", |_| deprecated::renamed("embassyd", "startd"));
self.bins
.insert("embassy-init", |_| deprecated::removed("embassy-init"));
self
}
pub fn enable_start_cli(&mut self) -> &mut Self {
self.bins.insert("start-cli", start_cli::main);
self.bins.insert("embassy-cli", |_| {
deprecated::renamed("embassy-cli", "start-cli")
});
self.bins
.insert("embassy-sdk", |_| deprecated::removed("embassy-sdk"));
self
}
pub fn enable_start_container(&mut self) -> &mut Self {
self.bins.insert("start-container", container_cli::main);
self
}
pub fn enable_start_registryd(&mut self) -> &mut Self {
self.bins.insert("start-registryd", registry::main);
self
}
pub fn enable_start_registry(&mut self) -> &mut Self {
self.bins.insert("start-registry", registry::cli);
self
}
pub fn enable_start_tunneld(&mut self) -> &mut Self {
self.bins.insert("start-tunneld", tunnel::main);
self
}
pub fn enable_start_tunnel(&mut self) -> &mut Self {
self.bins.insert("start-tunnel", tunnel::cli);
self
}
pub fn set_default(&mut self, name: &str) -> &mut Self {
if let Some((name, _)) = self.bins.get_key_value(name) {
self.default = Some(*name);
} else {
panic!("{}", t!("bins.mod.does-not-exist", name = name));
}
self
}
fn select_executable(&self, name: &str) -> Option<fn(VecDeque<OsString>)> {
self.bins.get(&name).copied()
}
pub fn execute(&self) {
set_locale_from_env();
let mut popped = Vec::with_capacity(2);
let mut args = std::env::args_os().collect::<VecDeque<_>>();
for _ in 0..2 {
if let Some(s) = args.pop_front() {
if let Some(name) = Path::new(&*s).file_name().and_then(|s| s.to_str()) {
if name == "--contents" {
for name in self.bins.keys() {
println!("{name}");
}
return;
}
if let Some(x) = self.select_executable(&name) {
args.push_front(s);
return x(args);
}
}
popped.push(s);
}
}
if let Some(default) = self.default {
while let Some(arg) = popped.pop() {
args.push_front(arg);
}
return self.bins[default](args);
}
let args = std::env::args().collect::<VecDeque<_>>();
eprintln!(
"{}",
t!(
"bins.mod.unknown-executable",
name = args
.get(1)
.or_else(|| args.get(0))
.map(|s| s.as_str())
.unwrap_or("N/A")
)
);
std::process::exit(1);
}
}

View File

@@ -1,101 +0,0 @@
use std::path::Path;
use tracing::instrument;
use crate::Error;
use crate::prelude::*;
use crate::util::Invoke;
pub async fn is_mountpoint(path: impl AsRef<Path>) -> Result<bool, Error> {
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(path.as_ref())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await?;
Ok(is_mountpoint.success())
}
#[instrument(skip_all)]
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
src: P0,
dst: P1,
read_only: bool,
) -> Result<(), Error> {
tracing::info!(
"{}",
t!(
"disk.mount.binding",
src = src.as_ref().display(),
dst = dst.as_ref().display()
)
);
if is_mountpoint(&dst).await? {
unmount(dst.as_ref(), true).await?;
}
tokio::fs::create_dir_all(&src).await?;
tokio::fs::create_dir_all(&dst).await?;
let mut mount_cmd = tokio::process::Command::new("mount");
mount_cmd.arg("--bind");
if read_only {
mount_cmd.arg("-o").arg("ro");
}
mount_cmd
.arg(src.as_ref())
.arg(dst.as_ref())
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}
#[instrument(skip_all)]
pub async fn unmount<P: AsRef<Path>>(mountpoint: P, lazy: bool) -> Result<(), Error> {
tracing::debug!("Unmounting {}.", mountpoint.as_ref().display());
let mut cmd = tokio::process::Command::new("umount");
if lazy {
cmd.arg("-l");
}
cmd.arg(mountpoint.as_ref())
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}
/// Unmounts all mountpoints under (and including) the given path, in reverse
/// depth order so that nested mounts are unmounted before their parents.
#[instrument(skip_all)]
pub async fn unmount_all_under<P: AsRef<Path>>(path: P, lazy: bool) -> Result<(), Error> {
let path = path.as_ref();
let canonical_path = tokio::fs::canonicalize(path)
.await
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("canonicalize {path:?}")))?;
let mounts_content = tokio::fs::read_to_string("/proc/mounts")
.await
.with_ctx(|_| (ErrorKind::Filesystem, "read /proc/mounts"))?;
// Collect all mountpoints under our path
let mut mountpoints: Vec<&str> = mounts_content
.lines()
.filter_map(|line| {
let mountpoint = line.split_whitespace().nth(1)?;
// Check if this mountpoint is under our target path
let mp_path = Path::new(mountpoint);
if mp_path.starts_with(&canonical_path) {
Some(mountpoint)
} else {
None
}
})
.collect();
// Sort by path length descending so we unmount deepest first
mountpoints.sort_by(|a, b| b.len().cmp(&a.len()));
for mountpoint in mountpoints {
tracing::debug!("Unmounting nested mountpoint: {}", mountpoint);
unmount(mountpoint, lazy).await?;
}
Ok(())
}

View File

@@ -1,207 +0,0 @@
use std::path::{Path, PathBuf};
use gpt::GptConfig;
use gpt::disk::LogicalBlockSize;
use tokio::process::Command;
use crate::disk::OsPartitionInfo;
use crate::os_install::partition_for;
use crate::prelude::*;
use crate::util::Invoke;
pub async fn partition(
disk_path: &Path,
capacity: u64,
protect: Option<&Path>,
use_efi: bool,
) -> Result<OsPartitionInfo, Error> {
// Guard: cannot protect the whole disk
if let Some(p) = protect {
if p == disk_path {
return Err(Error::new(
eyre!(
"Cannot protect the entire disk {}; must specify a partition",
disk_path.display()
),
crate::ErrorKind::InvalidRequest,
));
}
}
let disk_path = disk_path.to_owned();
let disk_path_clone = disk_path.clone();
let protect = protect.map(|p| p.to_owned());
let (efi, data_part) = tokio::task::spawn_blocking(move || {
let disk_path = disk_path_clone;
let protected_partition_info: Option<(u64, u64, PathBuf)> =
if let Some(ref protect_path) = protect {
let existing_gpt = GptConfig::new()
.writable(false)
.logical_block_size(LogicalBlockSize::Lb512)
.open_from_device(Box::new(
std::fs::File::options().read(true).open(&disk_path)?,
))?;
let info = existing_gpt
.partitions()
.iter()
.find(|(num, _)| partition_for(&disk_path, **num) == *protect_path)
.map(|(_, p)| (p.first_lba, p.last_lba, protect_path.clone()));
if info.is_none() {
return Err(Error::new(
eyre!(
"Protected partition {} not found in GPT on {}",
protect_path.display(),
disk_path.display()
),
crate::ErrorKind::NotFound,
));
}
info
} else {
None
};
let mut device = Box::new(
std::fs::File::options()
.read(true)
.write(true)
.open(&disk_path)?,
);
let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(
u32::try_from((capacity / 512) - 1).unwrap_or(0xFF_FF_FF_FF),
);
mbr.overwrite_lba0(&mut device)?;
let mut gpt = GptConfig::new()
.writable(true)
.logical_block_size(LogicalBlockSize::Lb512)
.create_from_device(device, None)?;
gpt.update_partitions(Default::default())?;
let efi = if use_efi {
gpt.add_partition("efi", 100 * 1024 * 1024, gpt::partition_types::EFI, 0, None)?;
true
} else {
gpt.add_partition(
"bios-grub",
8 * 1024 * 1024,
gpt::partition_types::BIOS,
0,
None,
)?;
false
};
gpt.add_partition(
"boot",
2 * 1024 * 1024 * 1024,
gpt::partition_types::LINUX_FS,
0,
None,
)?;
gpt.add_partition(
"root",
14 * 1024 * 1024 * 1024,
match crate::ARCH {
"x86_64" => gpt::partition_types::LINUX_ROOT_X64,
"aarch64" => gpt::partition_types::LINUX_ROOT_ARM_64,
_ => gpt::partition_types::LINUX_FS,
},
0,
None,
)?;
// Check if protected partition would be overwritten by OS partitions
if let Some((first_lba, _, ref path)) = protected_partition_info {
// Get the actual end sector of the last OS partition (root = partition 3)
let os_partitions_end_sector =
gpt.partitions().get(&3).map(|p| p.last_lba).unwrap_or(0);
if first_lba <= os_partitions_end_sector {
return Err(Error::new(
eyre!(
concat!(
"Protected partition {} starts at sector {}",
" which would be overwritten by OS partitions ending at sector {}"
),
path.display(),
first_lba,
os_partitions_end_sector
),
crate::ErrorKind::DiskManagement,
));
}
}
let data_part = if let Some((first_lba, last_lba, path)) = protected_partition_info {
// Re-create the data partition entry at the same location
let length_lba = last_lba - first_lba + 1;
let next_id = gpt.partitions().keys().max().map(|k| k + 1).unwrap_or(1);
gpt.add_partition_at(
"data",
next_id,
first_lba,
length_lba,
gpt::partition_types::LINUX_LVM,
0,
)?;
Some(path)
} else {
gpt.add_partition(
"data",
gpt.find_free_sectors()
.iter()
.map(|(_, size)| *size * u64::from(*gpt.logical_block_size()))
.max()
.ok_or_else(|| {
Error::new(
eyre!("No free space left on device"),
crate::ErrorKind::BlockDevice,
)
})?,
gpt::partition_types::LINUX_LVM,
0,
None,
)?;
gpt.partitions()
.last_key_value()
.map(|(num, _)| partition_for(&disk_path, *num))
};
gpt.write()?;
Ok::<_, Error>((efi, data_part))
})
.await
.unwrap()?;
// Re-read partition table and wait for udev to create device nodes
Command::new("vgchange")
.arg("-an")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("dmsetup")
.arg("remove_all")
.arg("--force")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("blockdev")
.arg("--rereadpt")
.arg(&disk_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("udevadm")
.arg("settle")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Ok(OsPartitionInfo {
efi: efi.then(|| partition_for(&disk_path, 1)),
bios: (!efi).then(|| partition_for(&disk_path, 1)),
boot: partition_for(&disk_path, 2),
root: partition_for(&disk_path, 3),
data: data_part,
})
}

View File

@@ -1,173 +0,0 @@
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
use mbrman::{CHS, MBR, MBRPartitionEntry};
use tokio::process::Command;
use crate::disk::OsPartitionInfo;
use crate::os_install::partition_for;
use crate::prelude::*;
use crate::util::Invoke;
pub async fn partition(
disk_path: &Path,
capacity: u64,
protect: Option<&Path>,
) -> Result<OsPartitionInfo, Error> {
// Guard: cannot protect the whole disk
if let Some(p) = protect {
if p == disk_path {
return Err(Error::new(
eyre!(
"Cannot protect the entire disk {}; must specify a partition",
disk_path.display()
),
crate::ErrorKind::InvalidRequest,
));
}
}
let disk_path = disk_path.to_owned();
let disk_path_clone = disk_path.clone();
let protect = protect.map(|p| p.to_owned());
let sectors = (capacity / 512) as u32;
let data_part = tokio::task::spawn_blocking(move || {
let disk_path = disk_path_clone;
// If protecting a partition, read its location from the existing MBR
let protected_partition_info: Option<(u32, u32, PathBuf)> =
if let Some(ref protect_path) = protect {
let mut file = std::fs::File::options().read(true).open(&disk_path)?;
let existing_mbr = MBR::read_from(&mut file, 512)?;
// Find the partition matching the protected path (check partitions 1-4)
let info = (1..=4u32)
.find(|&idx| partition_for(&disk_path, idx) == *protect_path)
.and_then(|idx| {
let entry = &existing_mbr[idx as usize];
if entry.sectors > 0 {
Some((entry.starting_lba, entry.sectors, protect_path.clone()))
} else {
None
}
});
if info.is_none() {
return Err(Error::new(
eyre!(
"Protected partition {} not found in MBR on {}",
protect_path.display(),
disk_path.display()
),
crate::ErrorKind::NotFound,
));
}
info
} else {
None
};
// MBR partition layout:
// Partition 1 (boot): starts at 2048, ends at 4196352 (sectors: 4194304 = 2GB)
// Partition 2 (root): starts at 4196352, ends at 33556480 (sectors: 29360128 = 14GB)
// OS partitions end at sector 33556480
let os_partitions_end_sector: u32 = 33556480;
// Check if protected partition would be overwritten
if let Some((starting_lba, _, ref path)) = protected_partition_info {
if starting_lba < os_partitions_end_sector {
return Err(Error::new(
eyre!(
concat!(
"Protected partition {} starts at sector {}",
" which would be overwritten by OS partitions ending at sector {}"
),
path.display(),
starting_lba,
os_partitions_end_sector
),
crate::ErrorKind::DiskManagement,
));
}
}
let mut file = std::fs::File::options()
.read(true)
.write(true)
.open(&disk_path)?;
let mut mbr = MBR::new_from(&mut file, 512, rand::random())?;
mbr[1] = MBRPartitionEntry {
boot: 0x80,
first_chs: CHS::empty(),
sys: 0x0b,
last_chs: CHS::empty(),
starting_lba: 2048,
sectors: 4196352 - 2048,
};
mbr[2] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x83,
last_chs: CHS::empty(),
starting_lba: 4196352,
sectors: 33556480 - 4196352,
};
let data_part = if let Some((starting_lba, part_sectors, path)) = protected_partition_info {
// Re-create the data partition entry at the same location
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba,
sectors: part_sectors,
};
Some(path)
} else {
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba: 33556480,
sectors: sectors - 33556480,
};
Some(partition_for(&disk_path, 3))
};
mbr.write_into(&mut file)?;
Ok::<_, Error>(data_part)
})
.await
.unwrap()?;
// Re-read partition table and wait for udev to create device nodes
Command::new("vgchange")
.arg("-an")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("dmsetup")
.arg("remove_all")
.arg("--force")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("blockdev")
.arg("--rereadpt")
.arg(&disk_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("udevadm")
.arg("settle")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Ok(OsPartitionInfo {
efi: None,
bios: None,
boot: partition_for(&disk_path, 1),
root: partition_for(&disk_path, 2),
data: data_part,
})
}

View File

@@ -1,311 +0,0 @@
use std::collections::BTreeMap;
use std::ops::Deref;
use axum::extract::Request;
use axum::response::Response;
use exver::{Version, VersionRange};
use http::HeaderValue;
use imbl_value::InternedString;
use patch_db::ModelExt;
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfoMap;
use crate::registry::package::get::{
GetPackageParams, GetPackageResponse, GetPackageResponseFull, PackageDetailLevel,
};
use crate::registry::package::index::PackageVersionInfo;
use crate::util::VersionString;
use crate::util::lshw::LshwDevice;
use crate::version::VersionT;
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
pub os: OsInfo,
pub hardware: Option<HardwareInfo>,
}
impl DeviceInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
Ok(Self {
os: OsInfo::from(ctx),
hardware: Some(HardwareInfo::load(ctx).await?),
})
}
}
impl DeviceInfo {
pub fn to_header_value(&self) -> HeaderValue {
let mut url: Url = "http://localhost".parse().unwrap();
let mut qp = url.query_pairs_mut();
qp.append_pair("os.version", &self.os.version.to_string())
.append_pair("os.compat", &self.os.compat.to_string())
.append_pair("os.platform", &*self.os.platform);
if let Some(lang) = self.os.language.as_deref() {
qp.append_pair("os.language", lang);
}
drop(qp);
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
}
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
let version = query
.get("os.version")
.or_not_found("os.version")?
.parse()?;
Ok(Self {
os: OsInfo {
compat: query.get("os.compat").or_not_found("os.compat")?.parse()?,
platform: query
.get("os.platform")
.or_not_found("os.platform")?
.deref()
.into(),
language: query
.get("os.language")
.map(|v| v.deref())
.map(InternedString::intern)
.or_else(|| {
if version < "0.4.0-alpha.18".parse().ok()? {
Some(rust_i18n::locale().deref().into())
} else {
None
}
}),
version,
},
hardware: has_hw_info
.then(|| {
Ok::<_, Error>(HardwareInfo {
arch: query
.get("hardware.arch")
.or_not_found("hardware.arch")?
.parse()?,
ram: query
.get("hardware.ram")
.or_not_found("hardware.ram")?
.parse()?,
devices: None,
})
})
.transpose()?,
})
}
pub fn filter_for_hardware(
&self,
method: &str,
params: Value,
res: &mut Value,
) -> Result<(), Error> {
match method {
"package.get" => {
let params: Model<GetPackageParams> = ModelExt::from_value(params);
let other = params.as_other_versions().de()?;
if params.as_id().transpose_ref().is_some() {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(res))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(res))?;
}
} else {
for (_, v) in res.as_object_mut().into_iter().flat_map(|o| o.iter_mut()) {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(v))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(v))?;
}
}
}
Ok(())
}
"os.version.get" => self.filter_os_version(ModelExt::value_as_mut(res)),
_ => Ok(()),
}
}
fn filter_package_versions(
&self,
versions: &mut Model<BTreeMap<VersionString, PackageVersionInfo>>,
) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter package versions using for_device
versions.retain(|_, info| info.for_device(self))?;
// Alpha.17 compatibility: add legacy fields
if self.os.version <= alpha_17 {
for (_, info) in versions.as_entries_mut()? {
let v = info.as_value_mut();
if let Some(mut tup) = v["s9pks"].get(0).cloned() {
v["s9pk"] = tup[1].take();
v["hardwareRequirements"] = tup[0].take();
v["s9pk"]["url"] = v["s9pk"]["urls"][0].clone();
}
}
}
Ok(())
}
fn filter_package_get(&self, res: &mut Model<GetPackageResponse>) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())
}
fn filter_package_get_full(
&self,
res: &mut Model<GetPackageResponseFull>,
) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())?;
self.filter_package_versions(res.as_other_versions_mut())
}
fn filter_os_version(&self, res: &mut Model<OsVersionInfoMap>) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter OS versions based on source_version compatibility
res.retain(|_, info| {
let source_version = info.as_source_version().de()?;
Ok(self.os.version.satisfies(&source_version))
})?;
// Alpha.17 compatibility: add url field from urls array
if self.os.version <= alpha_17 {
for (_, info) in res.as_entries_mut()? {
let v = info.as_value_mut();
for asset_ty in ["iso", "squashfs", "img"] {
for (_, asset) in v[asset_ty]
.as_object_mut()
.into_iter()
.flat_map(|o| o.iter_mut())
{
asset["url"] = asset["urls"][0].clone();
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct OsInfo {
#[ts(as = "VersionString")]
pub version: Version,
#[ts(type = "string")]
pub compat: VersionRange,
pub platform: InternedString,
pub language: Option<InternedString>,
}
impl From<&RpcContext> for OsInfo {
fn from(_: &RpcContext) -> Self {
Self {
version: crate::version::Current::default().semver(),
compat: crate::version::Current::default().compat().clone(),
platform: InternedString::intern(&*crate::PLATFORM),
language: Some(InternedString::intern(&*rust_i18n::locale())),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct HardwareInfo {
#[ts(type = "string")]
pub arch: InternedString,
#[ts(type = "number")]
pub ram: u64,
pub devices: Option<Vec<LshwDevice>>,
}
impl HardwareInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
let s = ctx.db.peek().await.into_public().into_server_info();
Ok(Self {
arch: s.as_arch().de()?,
ram: s.as_ram().de()?,
devices: Some(s.as_devices().de()?),
})
}
}
#[derive(Deserialize)]
pub struct Metadata {
#[serde(default)]
get_device_info: bool,
}
#[derive(Clone)]
pub struct DeviceInfoMiddleware {
device_info_header: Option<HeaderValue>,
device_info: Option<DeviceInfo>,
req: Option<RpcRequest>,
}
impl DeviceInfoMiddleware {
pub fn new() -> Self {
Self {
device_info_header: None,
device_info: None,
req: None,
}
}
}
impl Middleware<RegistryContext> for DeviceInfoMiddleware {
type Metadata = Metadata;
async fn process_http_request(
&mut self,
_: &RegistryContext,
request: &mut Request,
) -> Result<(), Response> {
self.device_info_header = request.headers_mut().remove(DEVICE_INFO_HEADER);
Ok(())
}
async fn process_rpc_request(
&mut self,
_: &RegistryContext,
metadata: Self::Metadata,
request: &mut RpcRequest,
) -> Result<(), RpcResponse> {
async move {
if metadata.get_device_info {
if let Some(device_info) = &self.device_info_header {
let device_info = DeviceInfo::from_header_value(device_info)?;
request.params["__DeviceInfo_device_info"] = to_value(&device_info)?;
self.device_info = Some(device_info);
self.req = Some(request.clone());
}
}
Ok::<_, Error>(())
}
.await
.map_err(|e| RpcResponse::from_result(Err(e)))
}
async fn process_rpc_response(
&mut self,
_: &RegistryContext,
response: &mut RpcResponse,
) -> () {
if let (Some(req), Some(device_info), Ok(res)) =
(&self.req, &self.device_info, &mut response.result)
{
if let Err(e) =
device_info.filter_for_hardware(req.method.as_str(), req.params.clone(), res)
{
response.result = Err(e).map_err(From::from);
}
}
}
}

View File

@@ -1,35 +0,0 @@
use imbl::vector;
use super::RegistryMigration;
use crate::prelude::*;
pub struct RegistryAssetArray;
impl RegistryMigration for RegistryAssetArray {
fn action(&self, db: &mut Value) -> Result<(), Error> {
for (_, info) in db["index"]["package"]["packages"]
.as_object_mut()
.unwrap()
.iter_mut()
{
for (_, info) in info["versions"].as_object_mut().unwrap().iter_mut() {
let hw_req = info["hardwareRequirements"].take();
let mut s9pk = info["s9pk"].take();
s9pk["urls"] = Value::Array(vector![s9pk["url"].take()]);
info["s9pks"] = Value::Array(vector![Value::Array(vector![hw_req, s9pk])]);
}
}
for (_, info) in db["index"]["os"]["versions"]
.as_object_mut()
.unwrap()
.iter_mut()
{
for asset_ty in ["iso", "squashfs", "img"] {
for (_, info) in info[asset_ty].as_object_mut().unwrap().iter_mut() {
info["urls"] = Value::Array(vector![info["url"].take()]);
}
}
}
Ok(())
}
}

View File

@@ -1,541 +0,0 @@
use std::path::PathBuf;
use std::sync::Arc;
use clap::Parser;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::HandlerArgs;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
use crate::PackageId;
use crate::context::CliContext;
use crate::prelude::*;
use crate::progress::FullProgressTracker;
use crate::registry::asset::BufferedHttpSource;
use crate::registry::context::RegistryContext;
use crate::registry::package::index::PackageVersionInfo;
use crate::s9pk::S9pk;
use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::sign::ed25519::Ed25519;
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
use crate::util::VersionString;
use crate::util::io::TrackingIO;
use crate::util::serde::Base64;
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddPackageParams {
#[ts(type = "string[]")]
pub urls: Vec<Url>,
#[ts(skip)]
#[serde(rename = "__Auth_signer")]
pub uploader: AnyVerifyingKey,
pub commitment: MerkleArchiveCommitment,
pub signature: AnySignature,
}
pub async fn add_package(
ctx: RegistryContext,
AddPackageParams {
urls,
uploader,
commitment,
signature,
}: AddPackageParams,
) -> Result<(), Error> {
uploader
.scheme()
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
let peek = ctx.db.peek().await;
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
let Some(([url], rest)) = urls.split_at_checked(1) else {
return Err(Error::new(
eyre!("{}", t!("registry.package.add.must-specify-url")),
ErrorKind::InvalidRequest,
));
};
let s9pk = S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
Some(&commitment),
)
.await?;
for url in rest {
S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
Some(&commitment),
)
.await?;
}
let manifest = s9pk.as_manifest();
let mut info = PackageVersionInfo::from_s9pk(&s9pk, urls).await?;
for (_, s9pk) in &mut info.s9pks {
if !s9pk.signatures.contains_key(&uploader) && s9pk.commitment == commitment {
s9pk.signatures.insert(uploader.clone(), signature.clone());
}
}
ctx.db
.mutate(|db| {
if db.as_admins().de()?.contains(&uploader_guid)
|| db
.as_index()
.as_package()
.as_packages()
.as_idx(&manifest.id)
.or_not_found(&manifest.id)?
.as_authorized()
.de()?
.get(&uploader_guid)
.map_or(false, |v| manifest.version.satisfies(v))
{
let package = db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.upsert(&manifest.id, || Ok(Default::default()))?;
let v = package.as_versions_mut();
if let Some(prev) = v.as_idx_mut(&manifest.version) {
prev.mutate(|p| p.merge_with(info, true))?;
} else {
v.insert(&manifest.version, &info)?;
}
Ok(())
} else {
Err(Error::new(
eyre!("{}", t!("registry.package.add.unauthorized")),
ErrorKind::Authorization,
))
}
})
.await
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddPackageParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(long, help = "help.arg.package-url")]
pub url: Vec<Url>,
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
pub async fn cli_add_package(
ctx: CliContext,
CliAddPackageParams {
file,
url,
no_verify,
}: CliAddPackageParams,
) -> Result<(), Error> {
let s9pk = S9pk::open(&file, None).await?;
cli_add_package_impl(ctx, s9pk, url, no_verify).await
}
pub async fn cli_add_package_impl(
ctx: CliContext,
s9pk: S9pk,
url: Vec<Url>,
no_verify: bool,
) -> Result<(), Error> {
let manifest = s9pk.as_manifest();
let progress = FullProgressTracker::new();
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
let verify = if !no_verify {
url.iter()
.map(|url| {
let phase = progress.add_phase(
InternedString::from_display(&lazy_format!("Verifying {url}")),
Some(100),
);
(url.clone(), phase)
})
.collect()
} else {
Vec::new()
};
let mut index_phase = progress.add_phase(
InternedString::intern("Adding File to Registry Index"),
Some(1),
);
let progress_task = progress.progress_bar_task(&format!(
"Adding {}@{}{} to registry...",
manifest.id,
manifest.version,
manifest
.hardware_requirements
.arch
.as_ref()
.map_or(String::new(), |a| format!(" ({})", a.iter().join("/")))
));
sign_phase.start();
let commitment = s9pk.as_archive().commitment().await?;
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
sign_phase.complete();
for (url, mut phase) in verify {
phase.start();
let source = BufferedHttpSource::new(ctx.client.clone(), url, phase).await?;
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true)
.await?;
}
index_phase.start();
ctx.call_remote::<RegistryContext>(
"package.add",
imbl_value::json!({
"urls": &url,
"signature": AnySignature::Ed25519(signature),
"commitment": commitment,
}),
)
.await?;
index_phase.complete();
progress.complete();
progress_task.await.with_kind(ErrorKind::Unknown)?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long, help = "help.arg.signature-hash")]
pub sighash: Option<Base64<[u8; 32]>>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}
pub async fn remove_package(
ctx: RegistryContext,
RemovePackageParams {
id,
version,
sighash,
signer,
}: RemovePackageParams,
) -> Result<bool, Error> {
let peek = ctx.db.peek().await;
let signer = signer.ok_or_else(|| {
Error::new(
eyre!("{}", t!("registry.package.missing-signer")),
ErrorKind::InvalidRequest,
)
})?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
let rev = ctx
.db
.mutate(|db| {
if db.as_admins().de()?.contains(&signer_guid)
|| db
.as_index()
.as_package()
.as_packages()
.as_idx(&id)
.or_not_found(&id)?
.as_authorized()
.de()?
.get(&signer_guid)
.map_or(false, |v| version.satisfies(v))
{
if let Some(package) = db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&id)
{
if let Some(sighash) = sighash {
if if let Some(package) = package.as_versions_mut().as_idx_mut(&version) {
package.as_s9pks_mut().mutate(|s| {
s.retain(|(_, asset)| asset.commitment.root_sighash != sighash);
Ok(s.is_empty())
})?
} else {
false
} {
package.as_versions_mut().remove(&version)?;
}
} else {
package.as_versions_mut().remove(&version)?;
}
}
Ok(())
} else {
Err(Error::new(
eyre!("{}", t!("registry.package.unauthorized")),
ErrorKind::Authorization,
))
}
})
.await;
rev.result.map(|_| rev.revision.is_some())
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddMirrorParams {
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
#[serde(rename = "__Auth_signer")]
pub uploader: AnyVerifyingKey,
pub commitment: MerkleArchiveCommitment,
pub signature: AnySignature,
}
pub async fn add_mirror(
ctx: RegistryContext,
AddMirrorParams {
url,
uploader,
commitment,
signature,
}: AddMirrorParams,
) -> Result<(), Error> {
uploader
.scheme()
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
let peek = ctx.db.peek().await;
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
let s9pk = S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
Some(&commitment),
)
.await?;
let manifest = s9pk.as_manifest();
let mut info = PackageVersionInfo::from_s9pk(&s9pk, vec![url]).await?;
for (_, s9pk) in &mut info.s9pks {
if !s9pk.signatures.contains_key(&uploader) && s9pk.commitment == commitment {
s9pk.signatures.insert(uploader.clone(), signature.clone());
}
}
ctx.db
.mutate(|db| {
if db.as_admins().de()?.contains(&uploader_guid)
|| db
.as_index()
.as_package()
.as_packages()
.as_idx(&manifest.id)
.or_not_found(&manifest.id)?
.as_authorized()
.de()?
.get(&uploader_guid)
.map_or(false, |v| manifest.version.satisfies(v))
{
let package = db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&manifest.id)
.and_then(|p| p.as_versions_mut().as_idx_mut(&manifest.version))
.or_not_found(&lazy_format!("{}@{}", &manifest.id, &manifest.version))?;
package.mutate(|p| p.merge_with(info, false))?;
Ok(())
} else {
Err(Error::new(
eyre!("{}", t!("registry.package.add-mirror.unauthorized")),
ErrorKind::Authorization,
))
}
})
.await
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddMirrorParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(help = "help.arg.mirror-url")]
pub url: Url,
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
pub async fn cli_add_mirror(
HandlerArgs {
context: ctx,
parent_method,
method,
params:
CliAddMirrorParams {
file,
url,
no_verify,
},
..
}: HandlerArgs<CliContext, CliAddMirrorParams>,
) -> Result<(), Error> {
let s9pk = S9pk::open(&file, None).await?;
let progress = FullProgressTracker::new();
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
let verify = if !no_verify {
let url = &url;
vec![(
url.clone(),
progress.add_phase(
InternedString::from_display(&lazy_format!("Verifying {url}")),
Some(100),
),
)]
} else {
Vec::new()
};
let mut index_phase = progress.add_phase(
InternedString::intern("Adding File to Registry Index"),
Some(1),
);
let progress_task =
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
sign_phase.start();
let commitment = s9pk.as_archive().commitment().await?;
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
sign_phase.complete();
for (url, mut phase) in verify {
phase.start();
let source = BufferedHttpSource::new(ctx.client.clone(), url, phase).await?;
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true)
.await?;
}
index_phase.start();
ctx.call_remote::<RegistryContext>(
&parent_method.into_iter().chain(method).join("."),
imbl_value::json!({
"url": &url,
"signature": AnySignature::Ed25519(signature),
"commitment": commitment,
}),
)
.await?;
index_phase.complete();
progress.complete();
progress_task.await.with_kind(ErrorKind::Unknown)?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveMirrorParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long, help = "help.arg.mirror-url")]
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}
pub async fn remove_mirror(
ctx: RegistryContext,
RemoveMirrorParams {
id,
version,
url,
signer,
}: RemoveMirrorParams,
) -> Result<(), Error> {
let peek = ctx.db.peek().await;
let signer = signer.ok_or_else(|| {
Error::new(
eyre!("{}", t!("registry.package.missing-signer")),
ErrorKind::InvalidRequest,
)
})?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
ctx.db
.mutate(|db| {
if db.as_admins().de()?.contains(&signer_guid)
|| db
.as_index()
.as_package()
.as_packages()
.as_idx(&id)
.or_not_found(&id)?
.as_authorized()
.de()?
.get(&signer_guid)
.map_or(false, |v| version.satisfies(v))
{
if let Some(package) = db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&id)
.and_then(|p| p.as_versions_mut().as_idx_mut(&version))
{
package.as_s9pks_mut().mutate(|s| {
s.iter_mut()
.for_each(|(_, asset)| asset.urls.retain(|u| u != &url));
if s.iter().any(|(_, asset)| asset.urls.is_empty()) {
Err(Error::new(
eyre!("{}", t!("registry.package.cannot-remove-last-mirror")),
ErrorKind::InvalidRequest,
))
} else {
Ok(())
}
})?;
}
Ok(())
} else {
Err(Error::new(
eyre!("{}", t!("registry.package.remove-mirror.unauthorized")),
ErrorKind::Authorization,
))
}
})
.await
.result
}

View File

@@ -1,314 +0,0 @@
use std::collections::{BTreeMap, BTreeSet};
use std::u32;
use chrono::Utc;
use exver::{Version, VersionRange};
use imbl_value::InternedString;
use patch_db::ModelExt;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
use crate::PackageId;
use crate::prelude::*;
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo;
use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements, LocaleString};
use crate::s9pk::merkle_archive::source::FileSource;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::sign::{AnySignature, AnyVerifyingKey};
use crate::util::{DataUrl, VersionString};
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct PackageIndex {
pub categories: BTreeMap<InternedString, Category>,
pub packages: BTreeMap<PackageId, PackageInfo>,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct PackageInfo {
#[ts(as = "BTreeMap::<Guid, String>")]
pub authorized: BTreeMap<Guid, VersionRange>,
pub versions: BTreeMap<VersionString, PackageVersionInfo>,
#[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct Category {
pub name: LocaleString,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DependencyMetadata {
pub title: Option<LocaleString>,
pub icon: Option<DataUrl<'static>>,
pub description: Option<LocaleString>,
pub optional: bool,
}
impl DependencyMetadata {
pub fn localize_for(&mut self, locale: &str) {
self.title.as_mut().map(|t| t.localize_for(locale));
self.description.as_mut().map(|d| d.localize_for(locale));
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct PackageMetadata {
#[ts(type = "string")]
pub title: InternedString,
pub icon: DataUrl<'static>,
pub description: Description,
pub release_notes: LocaleString,
pub git_hash: Option<GitHash>,
#[ts(type = "string")]
pub license: InternedString,
#[ts(type = "string")]
pub wrapper_repo: Url,
#[ts(type = "string")]
pub upstream_repo: Url,
#[ts(type = "string")]
pub support_site: Url,
#[ts(type = "string")]
pub marketing_site: Url,
#[ts(type = "string | null")]
pub donation_url: Option<Url>,
#[ts(type = "string | null")]
pub docs_url: Option<Url>,
pub alerts: Alerts,
pub dependency_metadata: BTreeMap<PackageId, DependencyMetadata>,
#[ts(type = "string")]
pub os_version: Version,
#[ts(type = "string | null")]
pub sdk_version: Option<Version>,
#[serde(default)]
pub hardware_acceleration: bool,
}
impl PackageMetadata {
pub async fn load<S: FileSource + Clone>(s9pk: &S9pk<S>) -> Result<Self, Error> {
let manifest = s9pk.as_manifest();
let mut dependency_metadata = BTreeMap::new();
for (id, info) in &manifest.dependencies.0 {
let metadata = s9pk.dependency_metadata(id).await?;
dependency_metadata.insert(
id.clone(),
DependencyMetadata {
title: metadata.map(|m| m.title),
icon: s9pk.dependency_icon_data_url(id).await?,
description: info.description.clone(),
optional: info.optional,
},
);
}
Ok(Self {
title: manifest.title.clone(),
icon: s9pk.icon_data_url().await?,
description: manifest.description.clone(),
release_notes: manifest.release_notes.clone(),
git_hash: manifest.git_hash.clone(),
license: manifest.license.clone(),
wrapper_repo: manifest.wrapper_repo.clone(),
upstream_repo: manifest.upstream_repo.clone(),
support_site: manifest.support_site.clone(),
marketing_site: manifest.marketing_site.clone(),
donation_url: manifest.donation_url.clone(),
docs_url: manifest.docs_url.clone(),
alerts: manifest.alerts.clone(),
dependency_metadata,
os_version: manifest.os_version.clone(),
sdk_version: manifest.sdk_version.clone(),
hardware_acceleration: manifest.hardware_acceleration.clone(),
})
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct PackageVersionInfo {
#[serde(flatten)]
pub metadata: PackageMetadata,
#[ts(type = "string | null")]
pub source_version: Option<VersionRange>,
pub s9pks: Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
}
impl PackageVersionInfo {
pub async fn from_s9pk<S: FileSource + Clone>(
s9pk: &S9pk<S>,
urls: Vec<Url>,
) -> Result<Self, Error> {
Ok(Self {
metadata: PackageMetadata::load(s9pk).await?,
source_version: None, // TODO
s9pks: vec![(
s9pk.as_manifest().hardware_requirements.clone(),
RegistryAsset {
published_at: Utc::now(),
urls,
commitment: s9pk.as_archive().commitment().await?,
signatures: [(
AnyVerifyingKey::Ed25519(s9pk.as_archive().signer()),
AnySignature::Ed25519(s9pk.as_archive().signature().await?),
)]
.into_iter()
.collect(),
},
)],
})
}
pub fn merge_with(&mut self, other: Self, replace_urls: bool) -> Result<(), Error> {
for (hw_req, asset) in other.s9pks {
if let Some((_, matching)) = self
.s9pks
.iter_mut()
.find(|(h, s)| s.commitment == asset.commitment && *h == hw_req)
{
if replace_urls {
matching.urls = asset.urls;
} else {
for url in asset.urls {
if matching.urls.contains(&url) {
continue;
}
matching.urls.push(url);
}
}
} else {
if let Some((h, matching)) = self.s9pks.iter_mut().find(|(h, _)| *h == hw_req) {
*matching = asset;
*h = hw_req;
} else {
self.s9pks.push((hw_req, asset));
}
}
}
self.s9pks.sort_by_key(|(h, _)| h.specificity_desc());
Ok(())
}
pub fn table(self, version: &VersionString) -> prettytable::Table {
use prettytable::*;
let mut table = Table::new();
table.add_row(row![bc => &self.metadata.title]);
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes.localized()]);
table.add_row(
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short.localized(), 80).join("\n")],
);
table.add_row(row![
br -> "DESCRIPTION",
&textwrap::wrap(&self.metadata.description.long.localized(), 80).join("\n")
]);
table.add_row(row![br -> "GIT HASH", self.metadata.git_hash.as_deref().unwrap_or("N/A")]);
table.add_row(row![br -> "LICENSE", &self.metadata.license]);
table.add_row(row![br -> "PACKAGE REPO", &self.metadata.wrapper_repo.to_string()]);
table.add_row(row![br -> "SERVICE REPO", &self.metadata.upstream_repo.to_string()]);
table.add_row(row![br -> "WEBSITE", &self.metadata.marketing_site.to_string()]);
table.add_row(row![br -> "SUPPORT", &self.metadata.support_site.to_string()]);
table
}
}
impl Model<PackageVersionInfo> {
/// Filters this package version for compatibility with the given device.
/// Returns false if the package is incompatible (should be removed).
/// Modifies s9pks in place to only include compatible variants.
pub fn for_device(&mut self, device_info: &DeviceInfo) -> Result<bool, Error> {
if !self
.as_metadata()
.as_os_version()
.de()?
.satisfies(&device_info.os.compat)
{
return Ok(false);
}
if let Some(hw) = &device_info.hardware {
self.as_s9pks_mut().mutate(|s9pks| {
s9pks.retain(|(hw_req, _)| {
if let Some(arch) = &hw_req.arch {
if !arch.contains(&hw.arch) {
return false;
}
}
if let Some(ram) = hw_req.ram {
if hw.ram < ram {
return false;
}
}
if let Some(dev) = &hw.devices {
for device_filter in &hw_req.device {
if !dev
.iter()
.filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.matches(d))
{
return false;
}
}
}
true
});
if hw.devices.is_some() {
s9pks.sort_by_key(|(req, _)| req.specificity_desc());
} else {
s9pks.sort_by_key(|(req, _)| {
let (dev, arch, ram) = req.specificity_desc();
(u32::MAX - dev, arch, ram)
});
}
Ok(())
})?;
if ModelExt::as_value(self.as_s9pks())
.as_array()
.map_or(true, |s| s.is_empty())
{
return Ok(false);
}
if let Some(locale) = device_info.os.language.as_deref() {
let metadata = self.as_metadata_mut();
metadata
.as_alerts_mut()
.mutate(|a| Ok(a.localize_for(locale)))?;
metadata
.as_dependency_metadata_mut()
.as_entries_mut()?
.into_iter()
.try_for_each(|(_, d)| d.mutate(|d| Ok(d.localize_for(locale))))?;
metadata
.as_description_mut()
.mutate(|d| Ok(d.localize_for(locale)))?;
metadata
.as_release_notes_mut()
.mutate(|r| Ok(r.localize_for(locale)))?;
}
}
Ok(true)
}
}
pub async fn get_package_index(ctx: RegistryContext) -> Result<PackageIndex, Error> {
ctx.db.peek().await.into_index().into_package().de()
}

View File

@@ -1,463 +0,0 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use clap::builder::ValueParserFactory;
use color_eyre::eyre::eyre;
use exver::{Version, VersionRange};
use imbl_value::{InOMap, InternedString};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
pub use crate::PackageId;
use crate::dependencies::Dependencies;
use crate::prelude::*;
use crate::s9pk::git_hash::GitHash;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex;
use crate::util::{FromStrParser, VersionString, mime};
use crate::version::{Current, VersionT};
use crate::{ImageId, VolumeId};
fn current_version() -> Version {
Current::default().semver()
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct Manifest {
pub id: PackageId,
#[ts(type = "string")]
pub title: InternedString,
pub version: VersionString,
pub satisfies: BTreeSet<VersionString>,
pub release_notes: LocaleString,
#[ts(type = "string")]
pub can_migrate_to: VersionRange,
#[ts(type = "string")]
pub can_migrate_from: VersionRange,
#[ts(type = "string")]
pub license: InternedString, // type of license
#[ts(type = "string")]
pub wrapper_repo: Url,
#[ts(type = "string")]
pub upstream_repo: Url,
#[ts(type = "string")]
pub support_site: Url,
#[ts(type = "string")]
pub marketing_site: Url,
#[ts(type = "string | null")]
pub donation_url: Option<Url>,
#[ts(type = "string | null")]
pub docs_url: Option<Url>,
pub description: Description,
pub images: BTreeMap<ImageId, ImageConfig>,
pub volumes: BTreeSet<VolumeId>,
#[serde(default)]
pub alerts: Alerts,
#[serde(default)]
pub dependencies: Dependencies,
#[serde(default)]
pub hardware_requirements: HardwareRequirements,
#[serde(default)]
pub hardware_acceleration: bool,
pub git_hash: Option<GitHash>,
#[serde(default = "current_version")]
#[ts(type = "string")]
pub os_version: Version,
#[ts(type = "string | null")]
pub sdk_version: Option<Version>,
}
impl Manifest {
pub fn validate_for<'a, T: Clone>(
&self,
arch: Option<&str>,
archive: &'a DirectoryContents<T>,
) -> Result<Filter, Error> {
let mut expected = Expected::new(archive);
expected.check_file("manifest.json")?;
expected.check_stem("icon", |ext| {
ext.and_then(|e| e.to_str())
.and_then(mime)
.map_or(false, |mime| mime.starts_with("image/"))
})?;
expected.check_file("LICENSE.md")?;
expected.check_file("javascript.squashfs")?;
for (dependency, _) in &self.dependencies.0 {
let dep_path = Path::new("dependencies").join(dependency);
let _ = expected.check_file(dep_path.join("metadata.json"));
let _ = expected.check_stem(dep_path.join("icon"), |ext| {
ext.and_then(|e| e.to_str())
.and_then(mime)
.map_or(false, |mime| mime.starts_with("image/"))
});
}
if let Err(e) = expected.check_file(Path::new("assets.squashfs")) {
// backwards compatibility for alpha s9pks - remove eventually
if expected.check_dir("assets").is_err() {
tracing::warn!("{e}");
tracing::debug!("{e:?}");
// return Err(e);
}
}
for (image_id, config) in &self.images {
let mut check_arch = |arch: &str| {
let mut arch = arch;
if let Err(e) = expected.check_file(
Path::new("images")
.join(arch)
.join(image_id)
.with_extension("squashfs"),
) {
if let Some(emulate_as) = &config.emulate_missing_as {
expected.check_file(
Path::new("images")
.join(emulate_as)
.join(image_id)
.with_extension("squashfs"),
)?;
arch = &**emulate_as;
} else {
return Err(e);
}
}
expected.check_file(
Path::new("images")
.join(arch)
.join(image_id)
.with_extension("json"),
)?;
expected.check_file(
Path::new("images")
.join(arch)
.join(image_id)
.with_extension("env"),
)?;
Ok(())
};
if let Some(arch) = arch {
check_arch(arch)?;
} else if let Some(arches) = &self.hardware_requirements.arch {
for arch in arches {
check_arch(arch)?;
}
} else if let Some(arch) = config.emulate_missing_as.as_deref() {
if !config.arch.contains(arch) {
return Err(Error::new(
eyre!("`emulateMissingAs` must match an included `arch`"),
ErrorKind::ParseS9pk,
));
}
for arch in &config.arch {
check_arch(&arch)?;
}
} else {
return Err(Error::new(
eyre!(
"`emulateMissingAs` required for all images if no `arch` specified in `hardwareRequirements`"
),
ErrorKind::ParseS9pk,
));
}
}
Ok(expected.into_filter())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct HardwareRequirements {
#[serde(default)]
pub device: Vec<DeviceFilter>,
#[ts(type = "number | null")]
pub ram: Option<u64>,
#[ts(type = "string[] | null")]
pub arch: Option<BTreeSet<InternedString>>,
}
impl HardwareRequirements {
/// returns a value that can be used as a sort key to get most specific requirements first
pub fn specificity_desc(&self) -> (u32, u32, u64) {
(
u32::MAX - self.device.len() as u32, // more device requirements = more specific
self.arch.as_ref().map_or(u32::MAX, |a| a.len() as u32), // more arches = less specific
self.ram.map_or(0, |r| r), // more ram = more specific
)
}
}
#[derive(Clone, Debug, PartialEq, TS)]
#[ts(type = "string | Record<string, string>")]
pub enum LocaleString {
Translated(String),
LanguageMap(InOMap<InternedString, String>),
}
impl std::str::FromStr for LocaleString {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Try JSON parse first (for maps or quoted strings)
if let Ok(parsed) = serde_json::from_str::<LocaleString>(s) {
return Ok(parsed);
}
// Fall back to plain string
Ok(LocaleString::Translated(s.to_owned()))
}
}
impl LocaleString {
pub fn localize_for(&mut self, locale: &str) {
if let Self::LanguageMap(map) = self {
if let Some(translated) = map.remove(locale) {
*self = Self::Translated(translated);
return;
}
let prefix = locale.split_inclusive("_").next().unwrap();
let mut first = None;
for (lang, translated) in std::mem::take(map) {
if lang.starts_with(prefix) {
*self = Self::Translated(translated);
return;
}
if first.is_none() {
first = Some(translated);
}
}
*self = Self::Translated(first.unwrap_or_default())
}
}
pub fn localized_for(mut self, locale: &str) -> String {
self.localize_for(locale);
if let Self::Translated(s) = self {
s
} else {
unreachable!()
}
}
pub fn localize(&mut self) {
self.localize_for(&*rust_i18n::locale());
}
pub fn localized(mut self) -> String {
self.localized_for(&*rust_i18n::locale())
}
}
impl<'de> Deserialize<'de> for LocaleString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LocaleStringVisitor;
impl<'de> serde::de::Visitor<'de> for LocaleStringVisitor {
type Value = LocaleString;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a map of language codes to strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LocaleString::Translated(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LocaleString::Translated(value))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let language_map =
InOMap::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
Ok(LocaleString::LanguageMap(language_map))
}
}
deserializer.deserialize_any(LocaleStringVisitor)
}
}
impl Serialize for LocaleString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
LocaleString::Translated(s) => serializer.serialize_str(s),
LocaleString::LanguageMap(map) => map.serialize(serializer),
}
}
}
impl ValueParserFactory for LocaleString {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DeviceFilter {
pub description: String,
#[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString,
#[ts(type = "string | null")]
pub product: Option<Regex>,
#[ts(type = "string | null")]
pub vendor: Option<Regex>,
#[ts(optional)]
pub capabilities: Option<BTreeSet<InternedString>>,
#[ts(optional)]
pub driver: Option<InternedString>,
}
// Omit description
impl PartialEq for DeviceFilter {
fn eq(&self, other: &Self) -> bool {
self.class == other.class
&& self.product == other.product
&& self.vendor == other.vendor
&& self.capabilities == other.capabilities
&& self.driver == other.driver
}
}
impl DeviceFilter {
pub fn matches(&self, device: &LshwDevice) -> bool {
if &*self.class != device.class() {
return false;
}
match device {
LshwDevice::Processor(LshwProcessor {
product,
vendor,
capabilities,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
true
}
LshwDevice::Display(LshwDisplay {
product,
vendor,
capabilities,
driver,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
if !self
.driver
.as_ref()
.map_or(true, |d| Some(d) == driver.as_ref())
{
return false;
}
true
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq)]
#[ts(export)]
pub struct Description {
pub short: LocaleString,
pub long: LocaleString,
}
impl Description {
pub fn localize_for(&mut self, locale: &str) {
self.short.localize_for(locale);
self.long.localize_for(locale);
}
pub fn validate(&self) -> Result<(), Error> {
if match &self.short {
LocaleString::Translated(s) => s.len() > 160,
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 160),
} {
return Err(Error::new(
eyre!("Short description must be 160 characters or less."),
crate::ErrorKind::ValidateS9pk,
));
}
if match &self.short {
LocaleString::Translated(s) => s.len() > 5000,
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 5000),
} {
return Err(Error::new(
eyre!("Long description must be 5000 characters or less."),
crate::ErrorKind::ValidateS9pk,
));
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Alerts {
pub install: Option<LocaleString>,
pub uninstall: Option<LocaleString>,
pub restore: Option<LocaleString>,
pub start: Option<LocaleString>,
pub stop: Option<LocaleString>,
}
impl Alerts {
pub fn localize_for(&mut self, locale: &str) {
self.install.as_mut().map(|s| s.localize_for(locale));
self.uninstall.as_mut().map(|s| s.localize_for(locale));
self.restore.as_mut().map(|s| s.localize_for(locale));
self.start.as_mut().map(|s| s.localize_for(locale));
self.stop.as_mut().map(|s| s.localize_for(locale));
}
}

View File

@@ -1,8 +0,0 @@
Section "InputClass"
Identifier "system-keyboard"
MatchIsKeyboard "on"
Option "XkbModel" "{model}"
Option "XkbLayout" "{layout}"
Option "XkbVariant" "{variant}"
Option "XkbOptions" "{options}"
EndSection

View File

@@ -1,7 +0,0 @@
XKBMODEL="{model}"
XKBLAYOUT="{layout}"
XKBVARIANT="{variant}"
XKBOPTIONS="{options}"
KEYMAP="{keymap}"

View File

@@ -1,97 +0,0 @@
use std::collections::BTreeSet;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use crate::prelude::*;
use crate::util::Invoke;
const KNOWN_CLASSES: &[&str] = &["processor", "display"];
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(tag = "class")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum LshwDevice {
Processor(LshwProcessor),
Display(LshwDisplay),
}
impl LshwDevice {
pub fn class(&self) -> &'static str {
match self {
Self::Processor(_) => "processor",
Self::Display(_) => "display",
}
}
#[instrument(skip_all)]
pub fn from_value(value: &Value) -> Option<Self> {
match value["class"].as_str() {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
Some("display") => Some(LshwDevice::Display(LshwDisplay::from_value(value))),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwProcessor {
pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
}
impl LshwProcessor {
#[instrument(skip_all)]
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwDisplay {
pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
pub driver: Option<InternedString>,
}
impl LshwDisplay {
#[instrument(skip_all)]
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
driver: value["configuration"]["driver"].as_str().map(From::from),
}
}
}
pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
let mut cmd = Command::new("lshw");
cmd.arg("-json");
for class in KNOWN_CLASSES {
cmd.arg("-class").arg(*class);
}
Ok(
serde_json::from_slice::<Vec<Value>>(&cmd.invoke(crate::ErrorKind::Lshw).await?)
.with_kind(crate::ErrorKind::Deserialization)?
.iter()
.filter_map(LshwDevice::from_value)
.collect(),
)
}

View File

@@ -1,53 +0,0 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_16};
use crate::db::model::public::AcmeSettings;
use crate::net::acme::AcmeProvider;
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_17: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 17.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_16::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_17.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
let acme = db["public"]["serverInfo"]["network"]["acme"]
.as_object_mut()
.or_not_found("public.serverInfo.network.acme")?;
let letsencrypt =
InternedString::intern::<&str>("letsencrypt".parse::<AcmeProvider>()?.as_ref());
if !acme.contains_key(&letsencrypt) {
acme.insert(
letsencrypt,
to_value(&AcmeSettings {
contact: Vec::new(),
})?,
);
}
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -1,67 +0,0 @@
use std::sync::Arc;
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_17};
use crate::context::RpcContext;
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_18: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 18.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_17::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_18.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
db["public"]["serverInfo"]["devices"] = Value::Array(Default::default());
let lang = db["public"]["ui"]
.as_object_mut()
.map_or(Value::Null, |m| m.remove("language").unwrap_or_default());
if let Some(lang) = lang.as_str() {
let lang = match lang {
"en" => "en_US",
"de" => "de_DE",
"es" => "es_ES",
"fr" => "fr_FR",
"pl" => "pl_PL",
_ => return Ok(Value::Null),
};
let lang = Value::String(Arc::new(lang.into()));
db["public"]["serverInfo"]["language"] = lang.clone();
return Ok(lang);
}
Ok(Value::Null)
}
async fn post_up(self, _: &RpcContext, input: Value) -> Result<(), Error> {
if let Some(language) = input.as_str() {
crate::system::save_language(language).await?;
}
Ok(())
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -1,37 +0,0 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_18};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_19: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 19.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_18::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_19.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

289
core/startos/Cargo.toml Normal file
View File

@@ -0,0 +1,289 @@
[package]
authors = ["Aiden McClelland <me@drbonez.dev>"]
description = "The core of StartOS"
documentation = "https://docs.rs/start-os"
edition = "2024"
keywords = [
"bitcoin",
"full-node",
"lightning",
"privacy",
"raspberry-pi",
"self-hosted",
]
license = "MIT"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.16" # VERSION_BUMP
[lib]
name = "startos"
path = "src/lib.rs"
[[bin]]
name = "startbox"
path = "src/main/startbox.rs"
[[bin]]
name = "start-cli"
path = "src/main/start-cli.rs"
[[bin]]
name = "start-container"
path = "src/main/start-container.rs"
[[bin]]
name = "registrybox"
path = "src/main/registrybox.rs"
[[bin]]
name = "tunnelbox"
path = "src/main/tunnelbox.rs"
[features]
arti = [
"arti-client",
"safelog",
"tor-cell",
"tor-hscrypto",
"tor-hsservice",
"tor-keymgr",
"tor-llcrypto",
"tor-proto",
"tor-rtcompat",
]
console = ["console-subscriber", "tokio/tracing"]
default = []
dev = []
test = []
unstable = ["backtrace-on-stack-overflow"]
[dependencies]
aes = { version = "0.7.5", features = ["ctr"] }
arti-client = { version = "0.33", features = [
"compression",
"ephemeral-keystore",
"experimental-api",
"onion-service-client",
"onion-service-service",
"rustls",
"static",
"tokio",
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls",
"use_tokio",
] }
async-compression = { version = "0.4.32", features = [
"brotli",
"gzip",
"tokio",
"zstd",
] }
async-stream = "0.3.5"
async-trait = "0.1.74"
axum = { version = "0.8.4", features = ["ws", "http2"] }
backtrace-on-stack-overflow = { version = "0.3.0", optional = true }
base32 = "0.5.0"
base64 = "0.22.1"
base64ct = "1.6.0"
basic-cookies = "0.1.4"
blake3 = { version = "1.5.0", features = ["mmap", "rayon"] }
bytes = "1"
chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.12", features = ["string"] }
color-eyre = "0.6.2"
console = "0.16.2"
console-subscriber = { version = "0.5.0", optional = true }
const_format = "0.2.34"
cookie = "0.18.0"
cookie_store = "0.22.0"
curve25519-dalek = "4.1.3"
der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7"
divrem = "1.0.0"
dns-lookup = "3.0.1"
ed25519 = { version = "2.2.3", features = ["alloc", "pem", "pkcs8"] }
ed25519-dalek = { version = "2.2.0", features = [
"digest",
"hazmat",
"pkcs8",
"rand_core",
"serde",
"zeroize",
] }
ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" }
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
"serde",
] }
fd-lock-rs = "0.1.4"
form_urlencoded = "1.2.1"
futures = "0.3.28"
gpt = "4.1.0"
hex = "0.4.3"
hickory-client = "0.25.2"
hickory-server = "0.25.2"
hmac = "0.12.1"
http = "1.0.0"
http-body-util = "0.1"
hyper = { version = "1.5", features = ["http1", "http2", "server"] }
hyper-util = { version = "0.1.10", features = [
"http1",
"http2",
"server",
"server-auto",
"server-graceful",
"service",
"tokio",
] }
id-pool = { version = "0.2.2", default-features = false, features = [
"serde",
"u16",
] }
iddqd = "0.3.14"
imbl = { version = "6", features = ["serde", "small-chunks"] }
imbl-value = { version = "0.4.3", features = ["ts-rs"] }
include_dir = { version = "0.7.3", features = ["metadata"] }
indexmap = { version = "2.0.2", features = ["serde"] }
indicatif = { version = "0.18.3", features = ["tokio"] }
inotify = "0.11.0"
integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
ipnet = { version = "2.8.0", features = ["serde"] }
isocountry = "0.3.2"
itertools = "0.14.0"
jaq-core = "0.10.1"
jaq-std = "0.10.0"
josekit = "0.10.3"
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
lazy_async_pool = "0.3.3"
lazy_format = "2.0"
lazy_static = "1.4.0"
lettre = { version = "0.11.18", default-features = false, features = [
"aws-lc-rs",
"builder",
"hostname",
"pool",
"rustls-platform-verifier",
"smtp-transport",
"tokio1-rustls",
] }
libc = "0.2.149"
log = "0.4.20"
mbrman = "0.6.0"
miette = { version = "7.6.0", features = ["fancy"] }
mio = "1"
new_mime_guess = "4"
nix = { version = "0.30.1", features = [
"fs",
"mount",
"net",
"process",
"sched",
"signal",
"user",
] }
nom = "8.0.0"
num = "0.4.1"
num_cpus = "1.16.0"
num_enum = "0.7.0"
once_cell = "1.19.0"
openssh-keys = "0.6.2"
openssl = { version = "0.10.57", features = ["vendored"] }
p256 = { version = "0.13.2", features = ["pem"] }
patch-db = { version = "*", path = "../../patch-db/patch-db", features = [
"trace",
] }
pbkdf2 = "0.12.2"
pin-project = "1.1.3"
pkcs8 = { version = "0.10.2", features = ["std"] }
prettytable-rs = "0.10.0"
proptest = "1.3.1"
proptest-derive = "0.7.0"
qrcode = "0.14.1"
r3bl_tui = "0.7.6"
rand = "0.9.2"
regex = "1.10.2"
reqwest = { version = "0.12.25", features = [
"json",
"socks",
"stream",
"http2",
] }
reqwest_cookie_store = "0.9.0"
rpassword = "7.2.0"
rust-argon2 = "3.0.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0"
serde_toml = { package = "toml", version = "0.9.9+spec-1.0.0" }
serde_yaml = { package = "serde_yml", version = "0.0.12" }
sha-crypt = "0.5.0"
sha2 = "0.10.2"
signal-hook = "0.3.17"
socket2 = { version = "0.6.0", features = ["all"] }
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
sqlx = { version = "0.8.6", features = [
"postgres",
"runtime-tokio-rustls",
], default-features = false }
sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40"
termion = "4.0.5"
textwrap = "0.16.1"
thiserror = "2.0.12"
tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.4"
tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] }
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] }
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hscrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-keymgr = { version = "0.33", features = [
"ephemeral-keystore",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-llcrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-rtcompat = { version = "0.33", features = [
"rustls",
"tokio",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
torut = "0.2.1"
tower-service = "0.3.3"
tracing = "0.1.39"
tracing-error = "0.2.0"
tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
ts-rs = "9.0.1"
typed-builder = "0.23.2"
url = { version = "2.4.1", features = ["serde"] }
uuid = { version = "1.4.1", features = ["v4"] }
visit-rs = "0.1.1"
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
zbus = "5.1.1"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18.0"
pty-process = "0.5.1"
[profile.test]
opt-level = 3
[profile.dev]
opt-level = 3
[profile.dev.package.backtrace]
opt-level = 3
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Pem = string
export interface SetStoreParams { value: any, path: string, }

View File

@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc dbb4790c31f9e400ed29a9ba2dbd61e3c55ce8a3fbae16601ca3512e803020ed # shrinks to files = []

View File

@@ -23,7 +23,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
"get-input",
from_fn_async(get_action_input)
.with_display_serializable()
.with_about("about.get-action-input-spec")
.with_about("Get action input spec")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -36,14 +36,14 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
}
Ok(())
})
.with_about("about.run-service-action")
.with_about("Run service action")
.with_call_remote::<CliContext>(),
)
.subcommand(
"clear-task",
from_fn_async(clear_task)
.no_display()
.with_about("about.clear-service-task")
.with_about("Clear a service task")
.with_call_remote::<CliContext>(),
)
}
@@ -63,9 +63,7 @@ pub struct ActionInput {
#[derive(Deserialize, Serialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
pub struct GetActionInputParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.action-id")]
pub action_id: ActionId,
}
@@ -282,11 +280,8 @@ pub struct RunActionParams {
#[derive(Parser)]
struct CliRunActionParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.event-id")]
pub event_id: Option<Guid>,
#[arg(help = "help.arg.action-id")]
pub action_id: ActionId,
#[command(flatten)]
pub input: StdinDeserializable<Option<Value>>,
@@ -365,11 +360,9 @@ pub async fn run_action(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ClearTaskParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.replay-id")]
pub replay_id: ReplayId,
#[arg(long, help = "help.arg.force-clear-task")]
#[arg(long)]
#[serde(default)]
pub force: bool,
}

View File

@@ -51,10 +51,7 @@ pub async fn write_shadow(password: &str) -> Result<(), Error> {
match line.split_once(":") {
Some((user, rest)) if user == "start9" || user == "kiosk" => {
let (_, rest) = rest.split_once(":").ok_or_else(|| {
Error::new(
eyre!("{}", t!("auth.malformed-etc-shadow")),
ErrorKind::ParseSysInfo,
)
Error::new(eyre!("malformed /etc/shadow"), ErrorKind::ParseSysInfo)
})?;
shadow_file
.write_all(format!("{user}:{hash}:{rest}\n").as_bytes())
@@ -84,7 +81,7 @@ impl PasswordType {
PasswordType::String(x) => Ok(x),
PasswordType::EncryptedWire(x) => x.decrypt(current_secret).ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("{}", t!("auth.couldnt-decode-password")),
color_eyre::eyre::eyre!("Couldn't decode password"),
crate::ErrorKind::Unknown,
)
}),
@@ -128,19 +125,19 @@ where
"login",
from_fn_async(cli_login::<AC>)
.no_display()
.with_about("about.login-new-auth-session"),
.with_about("Log in a new auth session"),
)
.subcommand(
"logout",
from_fn_async(logout::<AC>)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("about.logout-current-auth-session")
.with_about("Log out of current auth session")
.with_call_remote::<CliContext>(),
)
.subcommand(
"session",
session::<C, AC>().with_about("about.list-or-kill-auth-sessions"),
session::<C, AC>().with_about("List or kill auth sessions"),
)
.subcommand(
"reset-password",
@@ -150,14 +147,14 @@ where
"reset-password",
from_fn_async(cli_reset_password)
.no_display()
.with_about("about.reset-password"),
.with_about("Reset password"),
)
.subcommand(
"get-pubkey",
from_fn_async(get_pubkey)
.with_metadata("authenticated", Value::Bool(false))
.no_display()
.with_about("about.get-pubkey-from-server")
.with_about("Get public key derived from server private key")
.with_call_remote::<CliContext>(),
)
}
@@ -187,11 +184,7 @@ async fn cli_login<C: SessionAuthContext>(
where
CliContext: CallRemote<C>,
{
let password = if let Ok(password) = std::env::var("PASSWORD") {
password
} else {
rpassword::prompt_password("Password: ")?
};
let password = rpassword::prompt_password("Password: ")?;
ctx.call_remote::<C>(
&parent_method.into_iter().chain(method).join("."),
@@ -211,12 +204,12 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
ensure_code!(
argon2::verify_encoded(&hash, password.as_bytes()).map_err(|_| {
Error::new(
eyre!("{}", t!("auth.password-incorrect")),
eyre!("Password Incorrect"),
crate::ErrorKind::IncorrectPassword,
)
})?,
crate::ErrorKind::IncorrectPassword,
t!("auth.password-incorrect")
"Password Incorrect"
);
Ok(())
}
@@ -330,14 +323,14 @@ where
.with_metadata("get_session", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_sessions(handle.params, result))
.with_about("about.display-all-auth-sessions")
.with_about("Display all auth sessions")
.with_call_remote::<CliContext>(),
)
.subcommand(
"kill",
from_fn_async(kill::<AC>)
.no_display()
.with_about("about.terminate-auth-sessions")
.with_about("Terminate existing auth session(s)")
.with_call_remote::<CliContext>(),
)
}
@@ -421,7 +414,6 @@ impl AsLogoutSessionId for KillSessionId {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct KillParams {
#[arg(help = "help.arg.session-ids")]
ids: Vec<String>,
}
@@ -438,9 +430,7 @@ pub async fn kill<C: SessionAuthContext>(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ResetPasswordParams {
#[arg(help = "help.arg.old-password")]
old_password: Option<PasswordType>,
#[arg(help = "help.arg.new-password")]
new_password: Option<PasswordType>,
}
@@ -453,13 +443,13 @@ async fn cli_reset_password(
..
}: HandlerArgs<CliContext>,
) -> Result<(), RpcError> {
let old_password = rpassword::prompt_password(&t!("auth.prompt-current-password"))?;
let old_password = rpassword::prompt_password("Current Password: ")?;
let new_password = {
let new_password = rpassword::prompt_password(&t!("auth.prompt-new-password"))?;
if new_password != rpassword::prompt_password(&t!("auth.prompt-confirm"))? {
let new_password = rpassword::prompt_password("New Password: ")?;
if new_password != rpassword::prompt_password("Confirm: ")? {
return Err(Error::new(
eyre!("{}", t!("auth.passwords-do-not-match")),
eyre!("Passwords do not match"),
crate::ErrorKind::IncorrectPassword,
)
.into());
@@ -492,7 +482,7 @@ pub async fn reset_password_impl(
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("{}", t!("auth.password-incorrect")),
eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword,
));
}

View File

@@ -33,13 +33,11 @@ use crate::version::VersionT;
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct BackupParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(long = "old-password", help = "help.arg.old-backup-password")]
#[arg(long = "old-password")]
old_password: Option<crate::auth::PasswordType>,
#[arg(long = "package-ids", help = "help.arg.package-ids-to-backup")]
#[arg(long = "package-ids")]
package_ids: Option<Vec<PackageId>>,
#[arg(help = "help.arg.backup-password")]
password: crate::auth::PasswordType,
}
@@ -71,8 +69,8 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Success,
t!("backup.bulk.complete-title").to_string(),
t!("backup.bulk.complete-message").to_string(),
"Backup Complete".to_owned(),
"Your backup has completed".to_owned(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -90,8 +88,9 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Warning,
t!("backup.bulk.complete-title").to_string(),
t!("backup.bulk.complete-with-failures").to_string(),
"Backup Complete".to_owned(),
"Your backup has completed, but some package(s) failed to backup"
.to_owned(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -104,7 +103,7 @@ impl BackupStatusGuard {
.await
}
Err(e) => {
tracing::error!("{}", t!("backup.bulk.failed-error", error = e));
tracing::error!("Backup Failed: {}", e);
tracing::debug!("{:?}", e);
let err_string = e.to_string();
db.mutate(|db| {
@@ -112,8 +111,8 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Error,
t!("backup.bulk.failed-title").to_string(),
t!("backup.bulk.failed-message").to_string(),
"Backup Failed".to_owned(),
"Your backup failed to complete.".to_owned(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -225,7 +224,7 @@ fn assure_backing_up<'a>(
.as_backup_progress_mut();
if backing_up.transpose_ref().is_some() {
return Err(Error::new(
eyre!("{}", t!("backup.bulk.already-backing-up")),
eyre!("Server is already backing up!"),
ErrorKind::InvalidRequest,
));
}
@@ -304,7 +303,7 @@ async fn perform_backup(
let mut backup_guard = Arc::try_unwrap(backup_guard).map_err(|_| {
Error::new(
eyre!("{}", t!("backup.bulk.leaked-reference")),
eyre!("leaked reference to BackupMountGuard"),
ErrorKind::Incoherent,
)
})?;

View File

@@ -37,12 +37,12 @@ pub fn backup<C: Context>() -> ParentHandler<C> {
"create",
from_fn_async(backup_bulk::backup_all)
.no_display()
.with_about("about.create-backup-all-packages")
.with_about("Create backup for all packages")
.with_call_remote::<CliContext>(),
)
.subcommand(
"target",
target::target::<C>().with_about("about.commands-backup-target"),
target::target::<C>().with_about("Commands related to a backup target"),
)
}
@@ -51,7 +51,7 @@ pub fn package_backup<C: Context>() -> ParentHandler<C> {
"restore",
from_fn_async(restore::restore_packages_rpc)
.no_display()
.with_about("about.restore-packages-from-backup")
.with_about("Restore package(s) from backup")
.with_call_remote::<CliContext>(),
)
}

View File

@@ -23,19 +23,16 @@ use crate::progress::ProgressUnits;
use crate::s9pk::S9pk;
use crate::service::service_map::DownloadInstallFuture;
use crate::setup::SetupExecuteProgress;
use crate::system::{save_language, sync_kiosk};
use crate::util::serde::{IoFormat, Pem};
use crate::system::sync_kiosk;
use crate::util::serde::IoFormat;
use crate::{PLATFORM, PackageId};
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct RestorePackageParams {
#[arg(help = "help.arg.package-ids")]
pub ids: Vec<PackageId>,
#[arg(help = "help.arg.backup-target-id")]
pub target_id: BackupTargetId,
#[arg(help = "help.arg.backup-password")]
pub password: String,
}
@@ -66,10 +63,7 @@ pub async fn restore_packages_rpc(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::error!("Error restoring package {}: {}", id, err);
tracing::debug!("{:?}", err);
}
}
@@ -81,10 +75,10 @@ pub async fn restore_packages_rpc(
}
#[instrument(skip_all)]
pub async fn recover_full_server(
pub async fn recover_full_embassy(
ctx: &SetupContext,
disk_guid: InternedString,
password: String,
disk_guid: Arc<String>,
start_os_password: String,
recovery_source: TmpMountGuard,
server_id: &str,
recovery_password: &str,
@@ -108,7 +102,7 @@ pub async fn recover_full_server(
)?;
os_backup.account.password = argon2::hash_encoded(
password.as_bytes(),
start_os_password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::rfc9106_low_mem(),
)
@@ -117,32 +111,16 @@ pub async fn recover_full_server(
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
sync_kiosk(kiosk).await?;
let language = ctx.language.peek(|a| a.clone());
let keyboard = ctx.keyboard.peek(|a| a.clone());
if let Some(language) = &language {
save_language(&**language).await?;
}
if let Some(keyboard) = &keyboard {
keyboard.save().await?;
}
let db = ctx.db().await?;
db.put(
&ROOT,
&Database::init(&os_backup.account, kiosk, language, keyboard)?,
)
.await?;
db.put(&ROOT, &Database::init(&os_backup.account, kiosk)?)
.await?;
drop(db);
let config = ctx.config.peek(|c| c.clone());
let init_result = init(&ctx.webserver, &config, init_phases).await?;
let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
let rpc_ctx = RpcContext::init(
&ctx.webserver,
&config,
&ctx.config,
disk_guid.clone(),
Some(init_result),
rpc_ctx_phases,
@@ -167,10 +145,7 @@ pub async fn recover_full_server(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::error!("Error restoring package {}: {}", id, err);
tracing::debug!("{:?}", err);
}
}
@@ -180,14 +155,7 @@ pub async fn recover_full_server(
.await;
restore_phase.lock().await.complete();
Ok((
SetupResult {
hostname: os_backup.account.hostname,
root_ca: Pem(os_backup.account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
},
rpc_ctx,
))
Ok(((&os_backup.account).try_into()?, rpc_ctx))
}
#[instrument(skip(ctx, backup_guard))]

View File

@@ -52,21 +52,21 @@ pub fn cifs<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(add)
.no_display()
.with_about("about.add-new-backup-target")
.with_about("Add a new backup target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"update",
from_fn_async(update)
.no_display()
.with_about("about.update-existing-backup-target")
.with_about("Update an existing backup target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.no_display()
.with_about("about.remove-existing-backup-target")
.with_about("Remove an existing backup target")
.with_call_remote::<CliContext>(),
)
}
@@ -75,13 +75,9 @@ pub fn cifs<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct AddParams {
#[arg(help = "help.arg.cifs-hostname")]
pub hostname: String,
#[arg(help = "help.arg.cifs-path")]
pub path: PathBuf,
#[arg(help = "help.arg.cifs-username")]
pub username: String,
#[arg(help = "help.arg.cifs-password")]
pub password: Option<String>,
}
@@ -134,15 +130,10 @@ pub async fn add(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UpdateParams {
#[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId,
#[arg(help = "help.arg.cifs-hostname")]
pub hostname: String,
#[arg(help = "help.arg.cifs-path")]
pub path: PathBuf,
#[arg(help = "help.arg.cifs-username")]
pub username: String,
#[arg(help = "help.arg.cifs-password")]
pub password: Option<String>,
}
@@ -160,7 +151,7 @@ pub async fn update(
id
} else {
return Err(Error::new(
eyre!("{}", t!("backup.target.cifs.target-not-found", id = id)),
eyre!("Backup Target ID {} Not Found", id),
ErrorKind::NotFound,
));
};
@@ -180,13 +171,7 @@ pub async fn update(
.as_idx_mut(&id)
.ok_or_else(|| {
Error::new(
eyre!(
"{}",
t!(
"backup.target.cifs.target-not-found",
id = BackupTargetId::Cifs { id }
)
),
eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }),
ErrorKind::NotFound,
)
})?
@@ -210,7 +195,6 @@ pub async fn update(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct RemoveParams {
#[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId,
}
@@ -219,7 +203,7 @@ pub async fn remove(ctx: RpcContext, RemoveParams { id }: RemoveParams) -> Resul
id
} else {
return Err(Error::new(
eyre!("{}", t!("backup.target.cifs.target-not-found", id = id)),
eyre!("Backup Target ID {} Not Found", id),
ErrorKind::NotFound,
));
};
@@ -236,7 +220,7 @@ pub fn load(db: &DatabaseModel, id: u32) -> Result<Cifs, Error> {
.as_idx(&id)
.ok_or_else(|| {
Error::new(
eyre!("{}", t!("backup.target.cifs.target-not-found-id", id = id)),
eyre!("Backup Target ID {} Not Found", id),
ErrorKind::NotFound,
)
})?

View File

@@ -143,13 +143,13 @@ pub fn target<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"cifs",
cifs::cifs::<C>().with_about("about.add-remove-update-backup-target"),
cifs::cifs::<C>().with_about("Add, remove, or update a backup target"),
)
.subcommand(
"list",
from_fn_async(list)
.with_display_serializable()
.with_about("about.list-existing-backup-targets")
.with_about("List existing backup targets")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -159,20 +159,20 @@ pub fn target<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn::<CliContext, _>(|params, info| {
display_backup_info(params.params, info)
})
.with_about("about.display-package-backup-information")
.with_about("Display package backup information")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mount",
from_fn_async(mount)
.with_about("about.mount-backup-target")
.with_about("Mount backup target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"umount",
from_fn_async(umount)
.no_display()
.with_about("about.unmount-backup-target")
.with_about("Unmount backup target")
.with_call_remote::<CliContext>(),
)
}
@@ -268,11 +268,8 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) -> Re
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct InfoParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(help = "help.arg.server-id")]
server_id: String,
#[arg(help = "help.arg.backup-password")]
password: String,
}
@@ -308,13 +305,11 @@ lazy_static::lazy_static! {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct MountParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(long, help = "help.arg.server-id")]
#[arg(long)]
server_id: Option<String>,
#[arg(help = "help.arg.backup-password")]
password: String, // TODO: rpassword
#[arg(long, help = "help.arg.allow-partial-backup")]
#[arg(long)]
allow_partial: bool,
}
@@ -390,7 +385,6 @@ pub async fn mount(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UmountParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: Option<BackupTargetId>,
}

View File

@@ -17,7 +17,6 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)),
crate::service::effects::handler(),
)
.mutate_command(super::translate_cli)
.run(args)
{
match e.data {

Some files were not shown because too many files have changed in this diff Show More