fix --arch flag to fall back to emulation when native image unavailab… (#3108)

* fix --arch flag to fall back to emulation when native image unavailable, always infer hardware requirement for arch

* better handling of arch filter

* dont cancel in-progress commit workflows and abstract common setup

* cli improvements

fix group handling

* fix cli publish

* alpha.19

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2026-02-02 17:56:59 -07:00
committed by GitHub
parent 4f84073cb5
commit 989d5f73b1
32 changed files with 644 additions and 475 deletions

87
.github/actions/setup-build/action.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Setup Build Environment
description: Common build environment setup steps
inputs:
nodejs-version:
description: Node.js version
required: true
use-tmpfs:
description: Mount tmpfs (for fast runners)
required: false
default: "false"
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"
- name: Mount tmpfs
if: inputs.use-tmpfs == 'true'
shell: bash
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4
with:
submodules: recursive
- 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 }}
- 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

@@ -38,7 +38,7 @@ on:
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
@@ -48,6 +48,7 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -64,50 +65,10 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- 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' }}
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-build
with:
submodules: recursive
- 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 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 || '');
nodejs-version: ${{ env.NODEJS_VERSION }}
use-tmpfs: ${{ github.event.inputs.runner == 'fast' }}
- name: Make
run: TARGET=${{ matrix.triple }} make cli

View File

@@ -36,7 +36,7 @@ on:
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
@@ -46,6 +46,7 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -60,50 +61,10 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- 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' }}
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-build
with:
submodules: recursive
- 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 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 || '');
nodejs-version: ${{ env.NODEJS_VERSION }}
use-tmpfs: ${{ github.event.inputs.runner == 'fast' }}
- name: Make
run: make registry-deb

View File

@@ -36,7 +36,7 @@ on:
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
@@ -46,6 +46,7 @@ env:
jobs:
compile:
name: Build Debian Package
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -60,50 +61,10 @@ jobs:
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- 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' }}
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-build
with:
submodules: recursive
- 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 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 || '');
nodejs-version: ${{ env.NODEJS_VERSION }}
use-tmpfs: ${{ github.event.inputs.runner == 'fast' }}
- name: Make
run: make tunnel-deb

View File

@@ -46,7 +46,7 @@ on:
- next/*
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
@@ -56,6 +56,7 @@ env:
jobs:
compile:
name: Compile Base Binaries
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
@@ -90,54 +91,11 @@ jobs:
)[github.event.inputs.runner == 'fast']
}}
steps:
- 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' }}
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-build
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v5
with:
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 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 || '');
nodejs-version: ${{ env.NODEJS_VERSION }}
use-tmpfs: ${{ github.event.inputs.runner == 'fast' }}
setup-python: "true"
- name: Make
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar

View File

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

View File

@@ -46,6 +46,7 @@ openssh-server
podman
psmisc
qemu-guest-agent
qemu-user-static
rfkill
rsync
samba-common-bin

View File

@@ -85,9 +85,9 @@ 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")
- [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")
- [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

2
core/Cargo.lock generated
View File

@@ -7817,7 +7817,7 @@ dependencies = [
[[package]]
name = "start-os"
version = "0.4.0-alpha.18"
version = "0.4.0-alpha.19"
dependencies = [
"aes 0.7.5",
"arti-client",

View File

@@ -15,7 +15,7 @@ license = "MIT"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.18" # VERSION_BUMP
version = "0.4.0-alpha.19" # VERSION_BUMP
[lib]
name = "startos"
@@ -176,6 +176,7 @@ mio = "1"
new_mime_guess = "4"
nix = { version = "0.30.1", features = [
"fs",
"hostname",
"mount",
"net",
"process",

View File

@@ -1843,18 +1843,18 @@ service.mod.failed-to-parse-package-data-entry:
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
service.mod.no-matching-subcontainers:
en_US: "no matching subcontainers are running for %{id}; some possible choices are:\n%{subcontainers}"
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:\n%{subcontainers}"
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:\n%{subcontainers}"
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :\n%{subcontainers}"
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:\n%{subcontainers}"
en_US: "no matching subcontainers are running for %{id}; some possible choices are:"
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:"
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:"
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :"
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:"
service.mod.multiple-subcontainers-found:
en_US: "multiple subcontainers found for %{id}: \n%{subcontainer_ids}"
de_DE: "mehrere Subcontainer für %{id} gefunden: \n%{subcontainer_ids}"
es_ES: "se encontraron múltiples subcontenedores para %{id}: \n%{subcontainer_ids}"
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id} : \n%{subcontainer_ids}"
pl_PL: "znaleziono wiele podkontenerów dla %{id}: \n%{subcontainer_ids}"
en_US: "multiple subcontainers found for %{id}"
de_DE: "mehrere Subcontainer für %{id} gefunden"
es_ES: "se encontraron múltiples subcontenedores para %{id}"
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id}"
pl_PL: "znaleziono wiele podkontenerów dla %{id}"
service.mod.invalid-byte-length-for-signal:
en_US: "invalid byte length for signal: %{length}"
@@ -3703,6 +3703,20 @@ help.arg.wireguard-config:
fr_FR: "Configuration WireGuard"
pl_PL: "Konfiguracja WireGuard"
help.s9pk-s3base:
en_US: "Base URL for publishing s9pks"
de_DE: "Basis-URL für die Veröffentlichung von s9pks"
es_ES: "URL base para publicar s9pks"
fr_FR: "URL de base pour publier les s9pks"
pl_PL: "Bazowy URL do publikowania s9pks"
help.s9pk-s3bucket:
en_US: "S3 bucket to publish s9pks to (should correspond to s3base)"
de_DE: "S3-Bucket zum Veröffentlichen von s9pks (sollte mit s3base übereinstimmen)"
es_ES: "Bucket S3 para publicar s9pks (debe corresponder con s3base)"
fr_FR: "Bucket S3 pour publier les s9pks (doit correspondre à s3base)"
pl_PL: "Bucket S3 do publikowania s9pks (powinien odpowiadać s3base)"
# CLI command descriptions (about.*)
about.add-address-to-host:
en_US: "Add an address to this host"
@@ -4866,6 +4880,13 @@ about.persist-new-notification:
fr_FR: "Persister une nouvelle notification"
pl_PL: "Utrwal nowe powiadomienie"
about.publish-s9pk:
en_US: "Publish s9pk to S3 bucket and index on registry"
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
es_ES: "Publicar s9pk en bucket S3 e indexar en el registro"
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
about.rebuild-service-container:
en_US: "Rebuild service container"
de_DE: "Dienst-Container neu erstellen"

View File

@@ -38,6 +38,8 @@ pub struct CliContextSeed {
pub registry_url: Option<Url>,
pub registry_hostname: Vec<InternedString>,
pub registry_listen: Option<SocketAddr>,
pub s9pk_s3base: Option<Url>,
pub s9pk_s3bucket: Option<InternedString>,
pub tunnel_addr: Option<SocketAddr>,
pub tunnel_listen: Option<SocketAddr>,
pub client: Client,
@@ -129,6 +131,8 @@ impl CliContext {
.transpose()?,
registry_hostname: config.registry_hostname.unwrap_or_default(),
registry_listen: config.registry_listen,
s9pk_s3base: config.s9pk_s3base,
s9pk_s3bucket: config.s9pk_s3bucket,
tunnel_addr: config.tunnel,
tunnel_listen: config.tunnel_listen,
client: {

View File

@@ -68,6 +68,10 @@ pub struct ClientConfig {
pub registry_hostname: Option<Vec<InternedString>>,
#[arg(skip)]
pub registry_listen: Option<SocketAddr>,
#[arg(long, help = "help.s9pk-s3base")]
pub s9pk_s3base: Option<Url>,
#[arg(long, help = "help.s9pk-s3bucket")]
pub s9pk_s3bucket: Option<InternedString>,
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
pub tunnel: Option<SocketAddr>,
#[arg(skip)]
@@ -89,8 +93,13 @@ impl ContextConfig for ClientConfig {
self.host = self.host.take().or(other.host);
self.registry = self.registry.take().or(other.registry);
self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname);
self.registry_listen = self.registry_listen.take().or(other.registry_listen);
self.s9pk_s3base = self.s9pk_s3base.take().or(other.s9pk_s3base);
self.s9pk_s3bucket = self.s9pk_s3bucket.take().or(other.s9pk_s3bucket);
self.tunnel = self.tunnel.take().or(other.tunnel);
self.tunnel_listen = self.tunnel_listen.take().or(other.tunnel_listen);
self.proxy = self.proxy.take().or(other.proxy);
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
self.cookie_path = self.cookie_path.take().or(other.cookie_path);
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
}

View File

@@ -579,6 +579,7 @@ impl RpcContext {
pub async fn call_remote<RemoteContext>(
&self,
method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value,
) -> Result<Value, RpcError>
where
@@ -587,7 +588,7 @@ impl RpcContext {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
&self,
method,
OrdMap::new(),
metadata,
params,
Empty {},
)
@@ -596,20 +597,15 @@ impl RpcContext {
pub async fn call_remote_with<RemoteContext, T>(
&self,
method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value,
extra: T,
) -> Result<Value, RpcError>
where
Self: CallRemote<RemoteContext, T>,
{
<Self as CallRemote<RemoteContext, T>>::call_remote(
&self,
method,
OrdMap::new(),
params,
extra,
)
.await
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, metadata, params, extra)
.await
}
}
impl AsRef<Client> for RpcContext {

View File

@@ -4,7 +4,7 @@ use axum::http::StatusCode;
use axum::http::uri::InvalidUri;
use color_eyre::eyre::eyre;
use num_enum::TryFromPrimitive;
use patch_db::Revision;
use patch_db::Value;
use rpc_toolkit::reqwest;
use rpc_toolkit::yajrc::{
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
@@ -16,6 +16,7 @@ use tokio_rustls::rustls;
use ts_rs::TS;
use crate::InvalidId;
use crate::prelude::to_value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(i32)]
@@ -197,7 +198,7 @@ pub struct Error {
pub source: color_eyre::eyre::Error,
pub debug: Option<color_eyre::eyre::Error>,
pub kind: ErrorKind,
pub revision: Option<Revision>,
pub info: Value,
pub task: Option<JoinHandle<()>>,
}
@@ -228,7 +229,7 @@ impl Error {
source: source.into(),
debug,
kind,
revision: None,
info: Value::Null,
task: None,
}
}
@@ -237,7 +238,7 @@ impl Error {
source: eyre!("{}", self.source),
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
kind: self.kind,
revision: self.revision.clone(),
info: self.info.clone(),
task: None,
}
}
@@ -245,6 +246,10 @@ impl Error {
self.task = Some(task);
self
}
pub fn with_info(mut self, info: Value) -> Self {
self.info = info;
self
}
pub async fn wait(mut self) -> Self {
if let Some(task) = &mut self.task {
task.await.log_err();
@@ -423,6 +428,8 @@ impl From<patch_db::value::Error> for Error {
pub struct ErrorData {
pub details: String,
pub debug: String,
#[serde(default)]
pub info: Value,
}
impl Display for ErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -440,6 +447,7 @@ impl From<Error> for ErrorData {
Self {
details: value.to_string(),
debug: format!("{:?}", value),
info: value.info,
}
}
}
@@ -470,40 +478,31 @@ impl From<&RpcError> for ErrorData {
.or_else(|| d.as_str().map(|s| s.to_owned()))
})
.unwrap_or_else(|| value.message.clone().into_owned()),
info: to_value(
&value
.data
.as_ref()
.and_then(|d| d.as_object().and_then(|d| d.get("info"))),
)
.unwrap_or_default(),
}
}
}
impl From<Error> for RpcError {
fn from(e: Error) -> Self {
let mut data_object = serde_json::Map::with_capacity(3);
data_object.insert("details".to_owned(), format!("{}", e.source).into());
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
data_object.insert(
"revision".to_owned(),
match serde_json::to_value(&e.revision) {
let kind = e.kind;
let data = ErrorData::from(e);
RpcError {
code: kind as i32,
message: kind.as_str().into(),
data: Some(match serde_json::to_value(&data) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Error serializing revision for Error object: {}", e);
tracing::warn!("Error serializing ErrorData object: {}", e);
serde_json::Value::Null
}
},
);
RpcError {
code: e.kind as i32,
message: e.kind.as_str().into(),
data: Some(
match serde_json::to_value(&ErrorData {
details: format!("{}", e.source),
debug: format!("{:?}", e.source),
}) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Error serializing revision for Error object: {}", e);
serde_json::Value::Null
}
},
),
}),
}
}
}
@@ -606,7 +605,7 @@ where
kind,
source,
debug,
revision: None,
info: Value::Null,
task: None,
}
})

View File

@@ -131,6 +131,9 @@ pub async fn install(
let package: GetPackageResponse = from_value(
ctx.call_remote_with::<RegistryContext, _>(
"package.get",
[("get_device_info", Value::Bool(true))]
.into_iter()
.collect(),
json!({
"id": id,
"targetVersion": VersionRange::exactly(version.deref().clone()),

View File

@@ -540,7 +540,10 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.with_about("about.execute-commands-container")
.no_cli(),
)
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
.subcommand(
"attach",
from_fn_async_local(service::cli_attach).no_display(),
)
.subcommand(
"host",
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),

View File

@@ -135,20 +135,24 @@ pub struct CliAddPackageParams {
}
pub async fn cli_add_package(
HandlerArgs {
context: ctx,
parent_method,
method,
params:
CliAddPackageParams {
file,
url,
no_verify,
},
..
}: HandlerArgs<CliContext, CliAddPackageParams>,
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));
@@ -170,8 +174,16 @@ pub async fn cli_add_package(
Some(1),
);
let progress_task =
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
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?;
@@ -188,7 +200,7 @@ pub async fn cli_add_package(
index_phase.start();
ctx.call_remote::<RegistryContext>(
&parent_method.into_iter().chain(method).join("."),
"package.add",
imbl_value::json!({
"urls": &url,
"signature": AnySignature::Ed25519(signature),

View File

@@ -1,10 +1,13 @@
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;
use clap::Parser;
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use url::Url;
use crate::ImageId;
use crate::context::CliContext;
@@ -13,9 +16,9 @@ use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::v2::pack::ImageConfig;
use crate::util::Apply;
use crate::util::io::{TmpDir, create_file, open_file};
use crate::util::serde::{HandlerExtSerde, apply_expr};
use crate::util::{Apply, Invoke};
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
@@ -61,6 +64,12 @@ pub fn s9pk() -> ParentHandler<CliContext> {
.no_display()
.with_about("about.convert-s9pk-v1-to-v2"),
)
.subcommand(
"publish",
from_fn_async(publish)
.no_display()
.with_about("about.publish-s9pk"),
)
}
#[derive(Deserialize, Serialize, Parser)]
@@ -256,3 +265,61 @@ async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res
tokio::fs::rename(tmp_path, s9pk_path).await?;
Ok(())
}
async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
let filename = s9pk_path.file_name().unwrap().to_string_lossy();
let s9pk = super::S9pk::open(&s9pk_path, None).await?;
let manifest = s9pk.as_manifest();
let path = [
manifest.id.deref(),
manifest.version.as_str(),
filename.deref(),
];
let mut s3url = ctx
.s9pk_s3base
.as_ref()
.ok_or_else(|| Error::new(eyre!("--s9pk-s3base required"), ErrorKind::InvalidRequest))?
.clone();
s3url
.path_segments_mut()
.map_err(|_| {
Error::new(
eyre!("s9pk-s3base is invalid (missing protocol?)"),
ErrorKind::ParseUrl,
)
})?
.pop_if_empty()
.extend(path);
let mut s3dest = format!(
"s3://{}",
ctx.s9pk_s3bucket
.as_deref()
.or_else(|| s3url
.host_str()
.and_then(|h| h.split_once(".").map(|h| h.0)))
.ok_or_else(|| {
Error::new(eyre!("--s9pk-s3bucket required"), ErrorKind::InvalidRequest)
})?,
)
.parse::<Url>()?;
s3dest
.path_segments_mut()
.map_err(|_| {
Error::new(
eyre!("s9pk-s3base is invalid (missing protocol?)"),
ErrorKind::ParseUrl,
)
})?
.pop_if_empty()
.extend(path);
Command::new("s3cmd")
.arg("put")
.arg("-P")
.arg(s9pk_path)
.arg(s3dest.as_str())
.capture(false)
.invoke(ErrorKind::Network)
.await?;
crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await
}

View File

@@ -7,6 +7,7 @@ use clap::Parser;
use futures::future::{BoxFuture, ready};
use futures::{FutureExt, TryStreamExt};
use imbl_value::InternedString;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio::sync::OnceCell;
@@ -686,7 +687,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
let manifest = s9pk.as_manifest_mut();
manifest.git_hash = Some(GitHash::from_path(params.path()).await?);
if !params.arch.is_empty() {
let arches = match manifest.hardware_requirements.arch.take() {
let arches: BTreeSet<InternedString> = match manifest.hardware_requirements.arch.take() {
Some(a) => params
.arch
.iter()
@@ -695,10 +696,41 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
.collect(),
None => params.arch.iter().cloned().collect(),
};
manifest
.images
.values_mut()
.for_each(|c| c.arch = c.arch.intersection(&arches).cloned().collect());
if arches.is_empty() {
return Err(Error::new(
eyre!(
"none of the requested architectures ({:?}) are supported by this package",
params.arch
),
ErrorKind::InvalidRequest,
));
}
manifest.images.iter_mut().for_each(|(id, c)| {
let filtered = c
.arch
.intersection(&arches)
.cloned()
.collect::<BTreeSet<_>>();
if filtered.is_empty() {
if let Some(arch) = &c.emulate_missing_as {
tracing::warn!(
"ImageId {} is not available for {}, emulating as {}",
id,
arches.iter().join("/"),
arch
);
c.arch = [arch.clone()].into_iter().collect();
} else {
tracing::error!(
"ImageId {} is not available for {}",
id,
arches.iter().join("/"),
);
}
} else {
c.arch = filtered;
}
});
manifest.hardware_requirements.arch = Some(arches);
}

View File

@@ -10,6 +10,7 @@ use crate::rpc_continuations::Guid;
use crate::service::effects::prelude::*;
use crate::service::persistent_container::Subcontainer;
use crate::util::Invoke;
use crate::util::io::write_file_owned_atomic;
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
@@ -94,7 +95,7 @@ pub async fn create_subcontainer_fs(
.cloned()
{
let guid = Guid::new();
let rootfs_dir = context
let lxc_container = context
.seed
.persistent_container
.lxc_container
@@ -104,8 +105,9 @@ pub async fn create_subcontainer_fs(
eyre!("PersistentContainer has been destroyed"),
ErrorKind::Incoherent,
)
})?
.rootfs_dir();
})?;
let container_guid = &lxc_container.guid;
let rootfs_dir = lxc_container.rootfs_dir();
let mountpoint = rootfs_dir
.join("media/startos/subcontainers")
.join(guid.as_ref());
@@ -154,6 +156,20 @@ pub async fn create_subcontainer_fs(
.arg(&mountpoint)
.invoke(ErrorKind::Filesystem)
.await?;
write_file_owned_atomic(
mountpoint.join("etc/hostname"),
format!("{container_guid}\n"),
100000,
100000,
)
.await?;
write_file_owned_atomic(
mountpoint.join("etc/hosts"),
format!("127.0.0.1\tlocalhost\n127.0.1.1\t{container_guid}\n::1\tlocalhost ip6-localhost ip6-loopback\n"),
100000,
100000,
)
.await?;
tracing::info!("Mounted overlay {guid} for {image_id}");
context
.seed

View File

@@ -1,7 +1,6 @@
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString, c_int};
use std::fs::File;
use std::io::{IsTerminal, Read};
use std::io::{BufRead, BufReader, IsTerminal, Read};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
@@ -146,95 +145,160 @@ impl ExecParams {
let mut cmd = StdCommand::new(command);
let passwd = std::fs::read_to_string(chroot.join("etc/passwd"))
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))
.log_err()
.unwrap_or_default();
let mut home = None;
let mut uid = Err(None);
let mut gid = Err(None);
let mut needs_home = true;
if let Some((uid, gid)) =
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
Some((uid, uid))
} else if let Some((uid, gid)) = user
.as_deref()
.and_then(|u| u.split_once(":"))
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
{
Some((uid, gid))
} else if let Some(user) = user {
Some(
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
if user != split.next()? {
return None;
}
split.next(); // throw away x
let uid = split.next()?.parse().ok()?;
let gid = split.next()?.parse().ok()?;
split.next(); // throw away group name
home = split.next();
Some((uid, gid))
// uid gid
}) {
(uid, gid)
} else if user == "root" {
(0, 0)
} else {
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
},
)
if let Some(user) = user {
if let Some((u, g)) = user.split_once(":") {
uid = Err(Some(u));
gid = Err(Some(g));
} else {
None
uid = Err(Some(user));
}
{
if home.is_none() {
home = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
split.next(); // throw away user name
split.next(); // throw away x
if split.next()?.parse::<u32>().ok()? != uid {
return None;
}
split.next(); // throw away gid
split.next(); // throw away group name
split.next()
})
};
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
cmd.uid(uid);
cmd.gid(gid);
} else {
home = Some("/root");
}
cmd.env("HOME", home.unwrap_or("/"));
let env_string = if let Some(env_file) = &env_file {
std::fs::read_to_string(env_file)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
} else {
Default::default()
if let Some(u) = uid.err().flatten().and_then(|u| u.parse::<u32>().ok()) {
uid = Ok(u);
}
if let Some(g) = gid.err().flatten().and_then(|g| g.parse::<u32>().ok()) {
gid = Ok(g);
}
let mut update_env = |line: &str| {
if let Some((k, v)) = line.split_once("=") {
needs_home &= k != "HOME";
cmd.env(k, v);
} else {
tracing::warn!("Invalid line in env: {line}");
}
};
let env = env_string
.lines()
.chain(env.iter().map(|l| l.as_str()))
.map(|l| l.trim())
.filter_map(|l| l.split_once("="))
.collect::<BTreeMap<_, _>>();
if let Some(f) = env_file {
let mut lines = BufReader::new(
File::open(&f).with_ctx(|_| (ErrorKind::Filesystem, format!("open r {f:?}")))?,
)
.lines();
while let Some(line) = lines.next().transpose()? {
update_env(&line);
}
}
for line in env {
update_env(&line);
}
let needs_gid = Err(None) == gid;
let mut username = InternedString::intern("root");
let mut handle_passwd_line = |line: &str| -> Option<()> {
let l = line.trim();
let mut split = l.split(":");
let user = split.next()?;
match uid {
Err(Some(u)) if u != user => return None,
_ => (),
}
split.next(); // throw away x
let u: u32 = split.next()?.parse().ok()?;
match uid {
Err(Some(_)) => uid = Ok(u),
Err(None) if u == 0 => uid = Ok(u),
Ok(uid) if uid != u => return None,
_ => (),
}
username = user.into();
if !needs_gid && !needs_home {
return Some(());
}
let g = split.next()?;
if needs_gid {
gid = Ok(g.parse().ok()?);
}
if needs_home {
split.next(); // throw away group name
let home = split.next()?;
cmd.env("HOME", home);
}
Some(())
};
let mut lines = BufReader::new(
File::open(chroot.join("etc/passwd"))
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/passwd")))?,
)
.lines();
while let Some(line) = lines.next().transpose()? {
if handle_passwd_line(&line).is_some() {
break;
}
}
let mut groups = Vec::new();
let mut handle_group_line = |line: &str| -> Option<()> {
let l = line.trim();
let mut split = l.split(":");
let name = split.next()?;
split.next()?; // throw away x
let g = split.next()?.parse::<u32>().ok()?;
match gid {
Err(Some(n)) if n == name => gid = Ok(g),
_ => (),
}
let users = split.next()?;
if users.split(",").any(|u| u == &*username) {
groups.push(nix::unistd::Gid::from_raw(g));
}
Some(())
};
let mut lines = BufReader::new(
File::open(chroot.join("etc/group"))
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/group")))?,
)
.lines();
while let Some(line) = lines.next().transpose()? {
if handle_group_line(&line).is_none() {
tracing::warn!("Invalid /etc/group line: {line}");
}
}
std::os::unix::fs::chroot(chroot)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
if let Ok(uid) = uid {
if uid != 0 {
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
}
}
// Handle credential changes in pre_exec to control the order:
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
{
let set_uid = uid.ok();
let set_gid = gid.ok();
unsafe {
cmd.pre_exec(move || {
if !groups.is_empty() {
nix::unistd::setgroups(&groups)
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
if let Some(gid) = set_gid {
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
if let Some(uid) = set_uid {
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
Ok(())
});
}
}
cmd.args(args);
if let Some(workdir) = workdir {
cmd.current_dir(workdir);

View File

@@ -50,6 +50,7 @@ use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
use crate::util::net::WebSocket;
use crate::util::serde::Pem;
use crate::util::sync::SyncMutex;
use crate::util::tui::choose;
use crate::volume::data_dir;
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
@@ -709,6 +710,19 @@ pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Re
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SubcontainerInfo {
pub id: Guid,
pub name: InternedString,
pub image_id: ImageId,
}
impl std::fmt::Display for SubcontainerInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let SubcontainerInfo { id, name, image_id } = self;
write!(f, "{id} => Name: {name}; Image: {image_id}")
}
}
#[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct AttachParams {
@@ -722,7 +736,7 @@ pub struct AttachParams {
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
#[ts(type = "string | null")]
subcontainer: Option<InternedString>,
subcontainer: Option<Guid>,
#[ts(type = "string | null")]
name: Option<InternedString>,
#[ts(type = "string | null")]
@@ -745,7 +759,7 @@ pub async fn attach(
user,
}: AttachParams,
) -> Result<Guid, Error> {
let (container_id, subcontainer_id, image_id, workdir, root_command) = {
let (container_id, subcontainer_id, image_id, user, workdir, root_command) = {
let id = &id;
let service = ctx.services.get(id).await;
@@ -786,13 +800,6 @@ pub async fn attach(
}
})
.collect();
let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| {
format!(
"{guid} imageId: {image_id} name: \"{name}\"",
name = &wrapper.name,
image_id = &wrapper.image_id
)
};
let Some((subcontainer_id, image_id)) = subcontainer_ids
.first()
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
@@ -803,19 +810,17 @@ pub async fn attach(
.lock()
.await
.iter()
.map(format_subcontainer_pair)
.join("\n");
.map(|(g, s)| SubcontainerInfo {
id: g.clone(),
name: s.name.clone(),
image_id: s.image_id.clone(),
})
.collect::<Vec<_>>();
return Err(Error::new(
eyre!(
"{}",
t!(
"service.mod.no-matching-subcontainers",
id = id,
subcontainers = subcontainers
)
),
eyre!("{}", t!("service.mod.no-matching-subcontainers", id = id)),
ErrorKind::NotFound,
));
)
.with_info(to_value(&subcontainers)?));
};
let passwd = root_dir
@@ -835,38 +840,39 @@ pub async fn attach(
)
.with_kind(ErrorKind::Deserialization)?;
let root_command = get_passwd_command(
passwd,
user.as_deref()
.or_else(|| image_meta["user"].as_str())
.unwrap_or("root"),
)
.await;
let user = user
.clone()
.or_else(|| image_meta["user"].as_str().map(InternedString::intern))
.unwrap_or_else(|| InternedString::intern("root"));
let root_command = get_passwd_command(passwd, &*user).await;
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
if subcontainer_ids.len() > 1 {
let subcontainer_ids = subcontainer_ids
let subcontainers = subcontainer_ids
.into_iter()
.map(format_subcontainer_pair)
.join("\n");
.map(|(g, s)| SubcontainerInfo {
id: g.clone(),
name: s.name.clone(),
image_id: s.image_id.clone(),
})
.collect::<Vec<_>>();
return Err(Error::new(
eyre!(
"{}",
t!(
"service.mod.multiple-subcontainers-found",
id = id,
subcontainer_ids = subcontainer_ids
)
t!("service.mod.multiple-subcontainers-found", id = id,)
),
ErrorKind::InvalidRequest,
));
)
.with_info(to_value(&subcontainers)?));
}
(
service_ref.container_id()?,
subcontainer_id,
image_id,
user.into(),
workdir,
root_command,
)
@@ -883,7 +889,7 @@ pub async fn attach(
pty_size: Option<TermSize>,
image_id: ImageId,
workdir: Option<String>,
user: Option<InternedString>,
user: InternedString,
root_command: &RootCommand,
) -> Result<(), Error> {
use axum::extract::ws::Message;
@@ -904,11 +910,9 @@ pub async fn attach(
Path::new("/media/startos/images")
.join(image_id)
.with_extension("env"),
);
if let Some(user) = user {
cmd.arg("--user").arg(&*user);
}
)
.arg("--user")
.arg(&*user);
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
@@ -1091,45 +1095,6 @@ pub async fn attach(
Ok(guid)
}
#[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct ListSubcontainersParams {
pub id: PackageId,
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct SubcontainerInfo {
pub name: InternedString,
pub image_id: ImageId,
}
pub async fn list_subcontainers(
ctx: RpcContext,
ListSubcontainersParams { id }: ListSubcontainersParams,
) -> Result<BTreeMap<Guid, SubcontainerInfo>, Error> {
let service = ctx.services.get(&id).await;
let service_ref = service.as_ref().or_not_found(&id)?;
let container = &service_ref.seed.persistent_container;
let subcontainers = container.subcontainers.lock().await;
let result: BTreeMap<Guid, SubcontainerInfo> = subcontainers
.iter()
.map(|(guid, subcontainer)| {
(
guid.clone(),
SubcontainerInfo {
name: subcontainer.name.clone(),
image_id: subcontainer.image_id.clone(),
},
)
})
.collect();
Ok(result)
}
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
async {
let mut file = tokio::fs::File::open(etc_passwd_path).await?;
@@ -1210,23 +1175,34 @@ pub async fn cli_attach(
None
};
let method = parent_method.into_iter().chain(method).join(".");
let mut params = json!({
"id": params.id,
"command": params.command,
"tty": tty,
"stderrTty": stderr.is_terminal(),
"ptySize": if tty { TermSize::get_current() } else { None },
"subcontainer": params.subcontainer,
"imageId": params.image_id,
"name": params.name,
"user": params.user,
});
let guid: Guid = from_value(
context
.call_remote::<RpcContext>(
&parent_method.into_iter().chain(method).join("."),
json!({
"id": params.id,
"command": params.command,
"tty": tty,
"stderrTty": stderr.is_terminal(),
"ptySize": if tty { TermSize::get_current() } else { None },
"subcontainer": params.subcontainer,
"imageId": params.image_id,
"name": params.name,
"user": params.user,
}),
)
.await?,
match context
.call_remote::<RpcContext>(&method, params.clone())
.await
{
Ok(a) => a,
Err(e) => {
let prompt = e.to_string();
let options: Vec<SubcontainerInfo> = from_value(e.info)?;
let choice = choose(&prompt, &options).await?;
params["subcontainer"] = to_value(&choice.id)?;
context
.call_remote::<RpcContext>(&method, params.clone())
.await?
}
},
)?;
let mut ws = context.ws_continuation(guid).await?;

View File

@@ -6,6 +6,7 @@ use clap::{ArgAction, Parser};
use color_eyre::eyre::{Result, eyre};
use exver::{Version, VersionRange};
use futures::TryStreamExt;
use imbl::OrdMap;
use imbl_value::json;
use itertools::Itertools;
use patch_db::json_ptr::JsonPointer;
@@ -245,6 +246,7 @@ async fn maybe_do_update(
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
ctx.call_remote_with::<RegistryContext, _>(
"os.version.get",
OrdMap::new(),
json!({
"source": current_version,
"target": target,

View File

@@ -248,7 +248,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.or(Some(&res.stdout))
.filter(|a| !a.is_empty())
.and_then(|a| std::str::from_utf8(a).ok())
.unwrap_or(&format!("{} exited with code {}", cmd_str, res.status))
.unwrap_or(&format!("{} exited with {}", cmd_str, res.status))
);
Ok(res.stdout)
} else {
@@ -309,7 +309,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.filter(|a| !a.is_empty())
.and_then(|a| std::str::from_utf8(a).ok())
.unwrap_or(&format!(
"{} exited with code {}",
"{} exited with {}",
cmd.as_std().get_program().to_string_lossy(),
res.status
))

View File

@@ -58,8 +58,9 @@ mod v0_4_0_alpha_15;
mod v0_4_0_alpha_16;
mod v0_4_0_alpha_17;
mod v0_4_0_alpha_18;
mod v0_4_0_alpha_19;
pub type Current = v0_4_0_alpha_18::Version; // VERSION_BUMP
pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP
impl Current {
#[instrument(skip(self, db))]
@@ -179,7 +180,8 @@ enum Version {
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>), // VERSION_BUMP
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP
Other(exver::Version),
}
@@ -240,7 +242,8 @@ impl Version {
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
return Err(Error::new(
eyre!("unknown version {v}"),
@@ -293,7 +296,8 @@ impl Version {
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
}
}

View File

@@ -0,0 +1,37 @@
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(())
}
}

View File

@@ -132,7 +132,6 @@ export type SDKManifest = {
* `pattern` refers to a regular expression that at least one device of the specified class must match
* `patternDescription` is what will be displayed to the user about what kind of device is required
* @property {number} ram - Minimum RAM requirement (in megabytes MB)
* @property {string[]} arch - List of supported arches
* @example
* ```
hardwareRequirements: {
@@ -141,14 +140,12 @@ export type SDKManifest = {
{ class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' },
],
ram: 8192,
arch: ['x86-64'],
},
* ```
*/
readonly hardwareRequirements?: {
readonly device?: T.DeviceFilter[]
readonly ram?: number | null
readonly arch?: string[] | null
}
/**

View File

@@ -67,7 +67,7 @@ import {
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
import { Volumes, createVolumes } from "./util/Volume"
export const OSVersion = testTypeVersion("0.4.0-alpha.18")
export const OSVersion = testTypeVersion("0.4.0-alpha.19")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -42,11 +42,11 @@ export function buildManifest<
): Manifest & T.Manifest {
const images = Object.entries(manifest.images).reduce(
(images, [k, v]) => {
v.arch = v.arch || ["aarch64", "x86_64"]
v.arch = v.arch ?? ["aarch64", "x86_64", "riscv64"]
if (v.emulateMissingAs === undefined)
v.emulateMissingAs = (v.arch as string[]).includes("aarch64")
? "aarch64"
: v.arch[0] || null
v.emulateMissingAs = (v.arch as string[]).includes("x86_64")
? "x86_64"
: (v.arch[0] ?? null)
v.nvidiaContainer = !!v.nvidiaContainer
images[k] = v as ImageConfig
return images
@@ -75,21 +75,18 @@ export function buildManifest<
hardwareRequirements: {
device: manifest.hardwareRequirements?.device || [],
ram: manifest.hardwareRequirements?.ram || null,
arch:
manifest.hardwareRequirements?.arch === undefined
? Object.values(images).reduce(
(arch, inputSpec) => {
if (inputSpec.emulateMissingAs) {
return arch
}
if (arch === null) {
return inputSpec.arch
}
return arch.filter((a) => inputSpec.arch.includes(a))
},
null as string[] | null,
)
: manifest.hardwareRequirements?.arch,
arch: Object.values(images).reduce(
(arch, inputSpec) => {
if (inputSpec.emulateMissingAs) {
return arch
}
if (arch === null) {
return inputSpec.arch
}
return arch.filter((a) => inputSpec.arch.includes(a))
},
null as string[] | null,
),
},
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
}

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.18",
"version": "0.4.0-alpha.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "startos-ui",
"version": "0.4.0-alpha.18",
"version": "0.4.0-alpha.19",
"license": "MIT",
"dependencies": {
"@angular/animations": "^20.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.18",
"version": "0.4.0-alpha.19",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"license": "MIT",