hardware acceleration and support for NVIDIA cards on nonfree images (#3089)

* add nvidia packages

* add nvidia deps to nonfree

* gpu_acceleration flag & nvidia hacking

* fix gpu_config & /tmp/lxc.log

* implement hardware acceleration more dynamically

* refactor OpenUI

* use mknod

* registry updates for multi-hardware-requirements

* pluralize

* handle new registry types

* remove log

* migrations and driver fixes

* wip

* misc patches

* handle nvidia-container differently

* chore: comments (#3093)

* chore: comments

* revert some sizing

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* Revert "handle nvidia-container differently"

This reverts commit d708ae53df.

* fix debian containers

* cleanup

* feat: add empty array placeholder in forms (#3095)

* fixes from testing, client side device filtering for better fingerprinting resistance

* fix mac builds

---------

Co-authored-by: Sam Sartor <me@samsartor.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
Aiden McClelland
2026-01-15 11:42:17 -08:00
committed by GitHub
parent e8ef39adad
commit 99871805bd
95 changed files with 2758 additions and 1092 deletions

0
build/README.md Normal file
View File

View File

@@ -3,6 +3,7 @@ avahi-utils
b3sum b3sum
bash-completion bash-completion
beep beep
binfmt-support
bmon bmon
btrfs-progs btrfs-progs
ca-certificates ca-certificates
@@ -15,6 +16,7 @@ dnsutils
dosfstools dosfstools
e2fsprogs e2fsprogs
ecryptfs-utils ecryptfs-utils
equivs
exfatprogs exfatprogs
flashrom flashrom
fuse3 fuse3

View File

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

View File

@@ -0,0 +1,10 @@
+ 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

@@ -73,7 +73,7 @@ if [ "$NON_FREE" = 1 ]; then
if [ "$IB_SUITE" = "bullseye" ]; then if [ "$IB_SUITE" = "bullseye" ]; then
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free" ARCHIVE_AREAS="$ARCHIVE_AREAS non-free"
else else
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free-firmware" ARCHIVE_AREAS="$ARCHIVE_AREAS non-free non-free-firmware"
fi fi
fi fi
@@ -174,40 +174,123 @@ if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list
fi fi
cat > config/archives/backports.pref <<- EOF if [ "$NON_FREE" = 1 ]; then
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o config/archives/nvidia-container-toolkit.key
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/etc/apt/trusted.gpg.d/nvidia-container-toolkit.key.gpg] https://#g' \
> config/archives/nvidia-container-toolkit.list
fi
cat > config/archives/backports.pref <<-EOF
Package: linux-image-* Package: linux-image-*
Pin: release n=${IB_SUITE}-backports Pin: release n=${IB_SUITE}-backports
Pin-Priority: 500 Pin-Priority: 500
Package: linux-headers-*
Pin: release n=${IB_SUITE}-backports
Pin-Priority: 500
Package: *nvidia*
Pin: release n=${IB_SUITE}-backports
Pin-Priority: 500
EOF EOF
# Dependencies # Hooks
## Firmware
if [ "$NON_FREE" = 1 ]; then
echo 'firmware-iwlwifi firmware-misc-nonfree firmware-brcm80211 firmware-realtek firmware-atheros firmware-libertas firmware-amd-graphics' > config/package-lists/nonfree.list.chroot
fi
cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF
#!/bin/bash #!/bin/bash
set -e set -e
if [ "${NON_FREE}" = "1" ] && [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then
# install a specific NVIDIA driver version
# ---------------- configuration ----------------
NVIDIA_DRIVER_VERSION="\${NVIDIA_DRIVER_VERSION:-580.119.02}"
BASE_URL="https://download.nvidia.com/XFree86/Linux-${QEMU_ARCH}"
echo "[nvidia-hook] Using NVIDIA driver: \${NVIDIA_DRIVER_VERSION}" >&2
# ---------------- kernel version ----------------
# Determine target kernel version from newest /boot/vmlinuz-* in the chroot.
KVER="\$(
ls -1t /boot/vmlinuz-* 2>/dev/null \
| head -n1 \
| sed 's|.*/vmlinuz-||'
)"
if [ -z "\${KVER}" ]; then
echo "[nvidia-hook] ERROR: no /boot/vmlinuz-* found; cannot determine kernel version" >&2
exit 1
fi
echo "[nvidia-hook] Target kernel version: \${KVER}" >&2
# Ensure kernel headers are present
TEMP_APT_DEPS=(build-essential)
if [ ! -e "/lib/modules/\${KVER}/build" ]; then
TEMP_APT_DEPS+=(linux-headers-\${KVER})
fi
echo "[nvidia-hook] Installing build dependencies" >&2
/usr/lib/startos/scripts/install-equivs <<-EOF
Package: nvidia-depends
Version: \${NVIDIA_DRIVER_VERSION}
Section: unknown
Priority: optional
Depends: \${dep_list="\$(IFS=', '; echo "\${TEMP_APT_DEPS[*]}")"}
EOF
# ---------------- download and run installer ----------------
RUN_NAME="NVIDIA-Linux-${QEMU_ARCH}-\${NVIDIA_DRIVER_VERSION}.run"
RUN_PATH="/root/\${RUN_NAME}"
RUN_URL="\${BASE_URL}/\${NVIDIA_DRIVER_VERSION}/\${RUN_NAME}"
echo "[nvidia-hook] Downloading \${RUN_URL}" >&2
wget -O "\${RUN_PATH}" "\${RUN_URL}"
chmod +x "\${RUN_PATH}"
echo "[nvidia-hook] Running NVIDIA installer for kernel \${KVER}" >&2
sh "\${RUN_PATH}" \
--silent \
--kernel-name="\${KVER}" \
--no-x-check \
--no-nouveau-check \
--no-runlevel-check
# Rebuild module metadata
echo "[nvidia-hook] Running depmod for \${KVER}" >&2
depmod -a "\${KVER}"
echo "[nvidia-hook] NVIDIA \${NVIDIA_DRIVER_VERSION} installation complete for kernel \${KVER}" >&2
echo "[nvidia-hook] Removing build dependencies..." >&2
apt-get purge -y nvidia-depends
apt-get autoremove -y
echo "[nvidia-hook] Removed build dependencies." >&2
fi
cp /etc/resolv.conf /etc/resolv.conf.bak cp /etc/resolv.conf /etc/resolv.conf.bak
if [ "${IB_SUITE}" = trixie ] && [ "${IB_TARGET_ARCH}" != riscv64 ]; then if [ "${IB_SUITE}" = trixie ] && [ "${IB_TARGET_ARCH}" != riscv64 ]; then
echo 'deb https://deb.debian.org/debian/ bookworm main' > /etc/apt/sources.list.d/bookworm.list echo 'deb https://deb.debian.org/debian/ bookworm main' > /etc/apt/sources.list.d/bookworm.list
apt-get update apt-get update
apt-get install -y postgresql-15 apt-get install -y postgresql-15
rm /etc/apt/sources.list.d/bookworm.list rm /etc/apt/sources.list.d/bookworm.list
apt-get update apt-get update
systemctl mask postgresql systemctl mask postgresql
fi fi
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
ln -sf /usr/bin/pi-beep /usr/local/bin/beep ln -sf /usr/bin/pi-beep /usr/local/bin/beep
KERNEL_VERSION=${RPI_KERNEL_VERSION} sh /boot/config.sh > /boot/config.txt KERNEL_VERSION=${RPI_KERNEL_VERSION} sh /boot/config.sh > /boot/config.txt
mkinitramfs -c gzip -o initrd.img-${RPI_KERNEL_VERSION}-rpi-v8 ${RPI_KERNEL_VERSION}-rpi-v8 mkinitramfs -c gzip -o initrd.img-${RPI_KERNEL_VERSION}-rpi-v8 ${RPI_KERNEL_VERSION}-rpi-v8
mkinitramfs -c gzip -o initrd.img-${RPI_KERNEL_VERSION}-rpi-2712 ${RPI_KERNEL_VERSION}-rpi-2712 mkinitramfs -c gzip -o initrd.img-${RPI_KERNEL_VERSION}-rpi-2712 ${RPI_KERNEL_VERSION}-rpi-2712
fi fi
useradd --shell /bin/bash -G startos -m start9 useradd --shell /bin/bash -G startos -m start9
@@ -218,11 +301,11 @@ usermod -aG systemd-journal start9
echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd" echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd"
if [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then if [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then
/usr/lib/startos/scripts/enable-kiosk /usr/lib/startos/scripts/enable-kiosk
fi fi
if ! [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then if ! [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then
passwd -l start9 passwd -l start9
fi fi
EOF EOF
@@ -360,4 +443,4 @@ elif [ "${IMAGE_TYPE}" = img ]; then
fi fi
chown $IB_UID:$IB_UID $RESULTS_DIR/$IMAGE_BASENAME.* chown $IB_UID:$IB_UID $RESULTS_DIR/$IMAGE_BASENAME.*

View File

@@ -1 +1 @@
usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=startos usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory boot=startos

View File

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

View File

@@ -0,0 +1,20 @@
#!/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

@@ -1,87 +0,0 @@
#!/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

@@ -15,10 +15,10 @@ if [ "$SKIP_DL" != "1" ]; then
fi fi
if [ -n "$RUN_ID" ]; then if [ -n "$RUN_ID" ]; then
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree raspberrypi; do for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree raspberrypi; do
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
done done
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree 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 while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
done done
while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done
@@ -69,7 +69,7 @@ elif [ "$SKIP_UL" != "1" ]; then
fi fi
if [ "$SKIP_INDEX" != "1" ]; then if [ "$SKIP_INDEX" != "1" ]; then
for arch in aarch64 aarch64-nonfree x86_64 x86_64-nonfree; do for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree; do
for file in *_$arch.squashfs *_$arch.iso; 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://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g') start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g')
done done

661
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,67 +11,89 @@ pub mod startd;
pub mod tunnel; pub mod tunnel;
#[derive(Default)] #[derive(Default)]
pub struct MultiExecutable(BTreeMap<&'static str, fn(VecDeque<OsString>)>); pub struct MultiExecutable {
default: Option<&'static str>,
bins: BTreeMap<&'static str, fn(VecDeque<OsString>)>,
}
impl MultiExecutable { impl MultiExecutable {
pub fn enable_startd(&mut self) -> &mut Self { pub fn enable_startd(&mut self) -> &mut Self {
self.0.insert("startd", startd::main); self.bins.insert("startd", startd::main);
self.0 self.bins
.insert("embassyd", |_| deprecated::renamed("embassyd", "startd")); .insert("embassyd", |_| deprecated::renamed("embassyd", "startd"));
self.0 self.bins
.insert("embassy-init", |_| deprecated::removed("embassy-init")); .insert("embassy-init", |_| deprecated::removed("embassy-init"));
self self
} }
pub fn enable_start_cli(&mut self) -> &mut Self { pub fn enable_start_cli(&mut self) -> &mut Self {
self.0.insert("start-cli", start_cli::main); self.bins.insert("start-cli", start_cli::main);
self.0.insert("embassy-cli", |_| { self.bins.insert("embassy-cli", |_| {
deprecated::renamed("embassy-cli", "start-cli") deprecated::renamed("embassy-cli", "start-cli")
}); });
self.0 self.bins
.insert("embassy-sdk", |_| deprecated::removed("embassy-sdk")); .insert("embassy-sdk", |_| deprecated::removed("embassy-sdk"));
self self
} }
pub fn enable_start_container(&mut self) -> &mut Self { pub fn enable_start_container(&mut self) -> &mut Self {
self.0.insert("start-container", container_cli::main); self.bins.insert("start-container", container_cli::main);
self self
} }
pub fn enable_start_registryd(&mut self) -> &mut Self { pub fn enable_start_registryd(&mut self) -> &mut Self {
self.0.insert("start-registryd", registry::main); self.bins.insert("start-registryd", registry::main);
self self
} }
pub fn enable_start_registry(&mut self) -> &mut Self { pub fn enable_start_registry(&mut self) -> &mut Self {
self.0.insert("start-registry", registry::cli); self.bins.insert("start-registry", registry::cli);
self self
} }
pub fn enable_start_tunneld(&mut self) -> &mut Self { pub fn enable_start_tunneld(&mut self) -> &mut Self {
self.0.insert("start-tunneld", tunnel::main); self.bins.insert("start-tunneld", tunnel::main);
self self
} }
pub fn enable_start_tunnel(&mut self) -> &mut Self { pub fn enable_start_tunnel(&mut self) -> &mut Self {
self.0.insert("start-tunnel", tunnel::cli); 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!("{name} does not exist in MultiExecutable");
}
self self
} }
fn select_executable(&self, name: &str) -> Option<fn(VecDeque<OsString>)> { fn select_executable(&self, name: &str) -> Option<fn(VecDeque<OsString>)> {
self.0.get(&name).copied() self.bins.get(&name).copied()
} }
pub fn execute(&self) { pub fn execute(&self) {
let mut popped = Vec::with_capacity(2);
let mut args = std::env::args_os().collect::<VecDeque<_>>(); let mut args = std::env::args_os().collect::<VecDeque<_>>();
for _ in 0..2 { for _ in 0..2 {
if let Some(s) = args.pop_front() { if let Some(s) = args.pop_front() {
if let Some(name) = Path::new(&*s).file_name().and_then(|s| s.to_str()) { if let Some(name) = Path::new(&*s).file_name().and_then(|s| s.to_str()) {
if name == "--contents" { if name == "--contents" {
for name in self.0.keys() { for name in self.bins.keys() {
println!("{name}"); println!("{name}");
} }
return;
} }
if let Some(x) = self.select_executable(&name) { if let Some(x) = self.select_executable(&name) {
args.push_front(s); args.push_front(s);
return x(args); 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<_>>(); let args = std::env::args().collect::<VecDeque<_>>();
eprintln!( eprintln!(
"unknown executable: {}", "unknown executable: {}",

View File

@@ -7,6 +7,7 @@ use std::sync::Arc;
use cookie::{Cookie, Expiration, SameSite}; use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore; use cookie_store::CookieStore;
use http::HeaderMap; use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -238,10 +239,16 @@ impl CliContext {
where where
Self: CallRemote<RemoteContext>, Self: CallRemote<RemoteContext>,
{ {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {}) <Self as CallRemote<RemoteContext, Empty>>::call_remote(
.await &self,
.map_err(Error::from) method,
.with_ctx(|e| (e.kind, method)) OrdMap::new(),
params,
Empty {},
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
@@ -252,10 +259,16 @@ impl CliContext {
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra) <Self as CallRemote<RemoteContext, T>>::call_remote(
.await &self,
.map_err(Error::from) method,
.with_ctx(|e| (e.kind, method)) OrdMap::new(),
params,
extra,
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
} }
impl AsRef<Jwk> for CliContext { impl AsRef<Jwk> for CliContext {
@@ -292,7 +305,13 @@ impl AsRef<Client> for CliContext {
} }
impl CallRemote<RpcContext> for CliContext { impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await { if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store self.cookie_store
.lock() .lock()
@@ -319,7 +338,13 @@ impl CallRemote<RpcContext> for CliContext {
} }
} }
impl CallRemote<DiagnosticContext> for CliContext { impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -332,7 +357,13 @@ impl CallRemote<DiagnosticContext> for CliContext {
} }
} }
impl CallRemote<InitContext> for CliContext { impl CallRemote<InitContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -345,7 +376,13 @@ impl CallRemote<InitContext> for CliContext {
} }
} }
impl CallRemote<SetupContext> for CliContext { impl CallRemote<SetupContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -358,7 +395,13 @@ impl CallRemote<SetupContext> for CliContext {
} }
} }
impl CallRemote<InstallContext> for CliContext { impl CallRemote<InstallContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),

View File

@@ -15,6 +15,7 @@ use josekit::jwk::Jwk;
use reqwest::{Client, Proxy}; use reqwest::{Client, Proxy};
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{CallRemote, Context, Empty}; use rpc_toolkit::{CallRemote, Context, Empty};
use tokio::process::Command;
use tokio::sync::{RwLock, broadcast, oneshot, watch}; use tokio::sync::{RwLock, broadcast, oneshot, watch};
use tokio::time::Instant; use tokio::time::Instant;
use tracing::instrument; use tracing::instrument;
@@ -26,6 +27,10 @@ use crate::context::config::ServerConfig;
use crate::db::model::Database; use crate::db::model::Database;
use crate::db::model::package::TaskSeverity; use crate::db::model::package::TaskSeverity;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::{FileSystem, ReadOnly};
use crate::disk::mount::guard::MountGuard;
use crate::init::{InitResult, check_time_is_synchronized}; use crate::init::{InitResult, check_time_is_synchronized};
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
use crate::lxc::LxcManager; use crate::lxc::LxcManager;
@@ -41,12 +46,14 @@ use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations};
use crate::service::ServiceMap; use crate::service::ServiceMap;
use crate::service::action::update_tasks; use crate::service::action::update_tasks;
use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::effects::subcontainer::NVIDIA_OVERLAY_PATH;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::Invoke;
use crate::util::future::NonDetachingJoinHandle; use crate::util::future::NonDetachingJoinHandle;
use crate::util::io::delete_file; use crate::util::io::{TmpDir, delete_file};
use crate::util::lshw::LshwDevice; use crate::util::lshw::LshwDevice;
use crate::util::sync::{SyncMutex, SyncRwLock, Watch}; use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
use crate::{ActionId, DATA_DIR, PackageId}; use crate::{ActionId, DATA_DIR, PLATFORM, PackageId};
pub struct RpcContextSeed { pub struct RpcContextSeed {
is_closed: AtomicBool, is_closed: AtomicBool,
@@ -167,6 +174,124 @@ impl RpcContext {
init_net_ctrl.complete(); init_net_ctrl.complete();
tracing::info!("Initialized Net Controller"); tracing::info!("Initialized Net Controller");
if PLATFORM.ends_with("-nonfree") {
if let Err(e) = Command::new("nvidia-smi")
.invoke(ErrorKind::ParseSysInfo)
.await
{
tracing::warn!("nvidia-smi: {e}");
tracing::info!("The above warning can be ignored if no NVIDIA card is present");
} else {
async {
let version: InternedString = String::from_utf8(
Command::new("modinfo")
.arg("-F")
.arg("version")
.arg("nvidia")
.invoke(ErrorKind::ParseSysInfo)
.await?,
)?
.trim()
.into();
let nvidia_dir =
Path::new("/media/startos/data/package-data/nvidia").join(&*version);
// Generate single squashfs with both debian and generic overlays
let sqfs = nvidia_dir.join("container-overlay.squashfs");
if tokio::fs::metadata(&sqfs).await.is_err() {
let tmp = TmpDir::new().await?;
// Generate debian overlay (libs in /usr/lib/aarch64-linux-gnu/)
let debian_dir = tmp.join("debian");
tokio::fs::create_dir_all(&debian_dir).await?;
// Create /etc/debian_version to trigger debian path detection
tokio::fs::create_dir_all(debian_dir.join("etc")).await?;
tokio::fs::write(debian_dir.join("etc/debian_version"), "").await?;
let procfs = MountGuard::mount(
&Bind::new("/proc"),
debian_dir.join("proc"),
ReadOnly,
)
.await?;
Command::new("nvidia-container-cli")
.arg("configure")
.arg("--no-devbind")
.arg("--no-cgroups")
.arg("--utility")
.arg("--compute")
.arg("--graphics")
.arg("--video")
.arg(&debian_dir)
.invoke(ErrorKind::Unknown)
.await?;
procfs.unmount(true).await?;
// Run ldconfig to create proper symlinks for all NVIDIA libraries
Command::new("ldconfig")
.arg("-r")
.arg(&debian_dir)
.invoke(ErrorKind::Unknown)
.await?;
// Remove /etc/debian_version - it was only needed for nvidia-container-cli detection
tokio::fs::remove_file(debian_dir.join("etc/debian_version")).await?;
// Generate generic overlay (libs in /usr/lib64/)
let generic_dir = tmp.join("generic");
tokio::fs::create_dir_all(&generic_dir).await?;
// No /etc/debian_version - will use generic /usr/lib64 paths
let procfs = MountGuard::mount(
&Bind::new("/proc"),
generic_dir.join("proc"),
ReadOnly,
)
.await?;
Command::new("nvidia-container-cli")
.arg("configure")
.arg("--no-devbind")
.arg("--no-cgroups")
.arg("--utility")
.arg("--compute")
.arg("--graphics")
.arg("--video")
.arg(&generic_dir)
.invoke(ErrorKind::Unknown)
.await?;
procfs.unmount(true).await?;
// Run ldconfig to create proper symlinks for all NVIDIA libraries
Command::new("ldconfig")
.arg("-r")
.arg(&generic_dir)
.invoke(ErrorKind::Unknown)
.await?;
// Create squashfs with UID/GID mapping (avoids chown on readonly mounts)
if let Some(p) = sqfs.parent() {
tokio::fs::create_dir_all(p)
.await
.with_ctx(|_| (ErrorKind::Filesystem, format!("mkdir -p {p:?}")))?;
}
Command::new("mksquashfs")
.arg(&*tmp)
.arg(&sqfs)
.arg("-force-uid")
.arg("100000")
.arg("-force-gid")
.arg("100000")
.invoke(ErrorKind::Filesystem)
.await?;
tmp.unmount_and_delete().await?;
}
BlockDev::new(&sqfs)
.mount(NVIDIA_OVERLAY_PATH, ReadOnly)
.await?;
Ok::<_, Error>(())
}
.await
.log_err();
}
}
let services = ServiceMap::default(); let services = ServiceMap::default();
let metrics_cache = Watch::<Option<crate::system::Metrics>>::new(None); let metrics_cache = Watch::<Option<crate::system::Metrics>>::new(None);
let socks_proxy_url = format!("socks5h://{socks_proxy}"); let socks_proxy_url = format!("socks5h://{socks_proxy}");
@@ -460,8 +585,14 @@ impl RpcContext {
where where
Self: CallRemote<RemoteContext>, Self: CallRemote<RemoteContext>,
{ {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {}) <Self as CallRemote<RemoteContext, Empty>>::call_remote(
.await &self,
method,
OrdMap::new(),
params,
Empty {},
)
.await
} }
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
@@ -472,7 +603,14 @@ impl RpcContext {
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await <Self as CallRemote<RemoteContext, T>>::call_remote(
&self,
method,
OrdMap::new(),
params,
extra,
)
.await
} }
} }
impl AsRef<Client> for RpcContext { impl AsRef<Client> for RpcContext {

View File

@@ -416,6 +416,51 @@ impl<T: Map> Model<T> {
} }
} }
impl<T: Map> Model<T>
where
T::Key: FromStr,
Error: From<<T::Key as FromStr>::Err>,
{
/// Retains only the elements specified by the predicate.
/// The predicate can mutate the values and returns whether to keep each entry.
pub fn retain<F>(&mut self, mut f: F) -> Result<(), Error>
where
F: FnMut(&T::Key, &mut Model<T::Value>) -> Result<bool, Error>,
{
let mut to_remove = Vec::new();
match &mut self.value {
Value::Object(o) => {
for (k, v) in o.iter_mut() {
let key = T::Key::from_str(&**k)?;
if !f(&key, patch_db::ModelExt::value_as_mut(v))? {
to_remove.push(k.clone());
}
}
}
v => {
use serde::de::Error;
return Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!(
"expected object found {v}"
)),
kind: patch_db::value::ErrorKind::Deserialization,
}
.into());
}
}
// Remove entries that didn't pass the filter
if let Value::Object(o) = &mut self.value {
for k in to_remove {
o.remove(&k);
}
}
Ok(())
}
}
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct JsonKey<T>(pub T); pub struct JsonKey<T>(pub T);

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use digest::generic_array::GenericArray; use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser}; use digest::{Digest, OutputSizeUser};
use itertools::Itertools;
use sha2::Sha256; use sha2::Sha256;
use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite}; use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite};
@@ -12,12 +13,13 @@ use crate::prelude::*;
use crate::util::io::TmpDir; use crate::util::io::TmpDir;
pub struct OverlayFs<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> { pub struct OverlayFs<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> {
lower: P0, lower: Vec<P0>,
upper: P1, upper: P1,
work: P2, work: P2,
} }
impl<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> OverlayFs<P0, P1, P2> { impl<P0: AsRef<Path>, P1: AsRef<Path>, P2: AsRef<Path>> OverlayFs<P0, P1, P2> {
pub fn new(lower: P0, upper: P1, work: P2) -> Self { /// layers are top to bottom
pub fn new(lower: Vec<P0>, upper: P1, work: P2) -> Self {
Self { lower, upper, work } Self { lower, upper, work }
} }
} }
@@ -32,8 +34,10 @@ impl<P0: AsRef<Path> + Send + Sync, P1: AsRef<Path> + Send + Sync, P2: AsRef<Pat
} }
fn mount_options(&self) -> impl IntoIterator<Item = impl Display> { fn mount_options(&self) -> impl IntoIterator<Item = impl Display> {
[ [
Box::new(lazy_format!("lowerdir={}", self.lower.as_ref().display())) Box::new(lazy_format!(
as Box<dyn Display>, "lowerdir={}",
self.lower.iter().map(|p| p.as_ref().display()).join(":")
)) as Box<dyn Display>,
Box::new(lazy_format!("upperdir={}", self.upper.as_ref().display())), Box::new(lazy_format!("upperdir={}", self.upper.as_ref().display())),
Box::new(lazy_format!("workdir={}", self.work.as_ref().display())), Box::new(lazy_format!("workdir={}", self.work.as_ref().display())),
] ]
@@ -51,18 +55,21 @@ impl<P0: AsRef<Path> + Send + Sync, P1: AsRef<Path> + Send + Sync, P2: AsRef<Pat
tokio::fs::create_dir_all(self.work.as_ref()).await?; tokio::fs::create_dir_all(self.work.as_ref()).await?;
let mut sha = Sha256::new(); let mut sha = Sha256::new();
sha.update("OverlayFs"); sha.update("OverlayFs");
sha.update( for lower in &self.lower {
tokio::fs::canonicalize(self.lower.as_ref()) sha.update(
.await tokio::fs::canonicalize(lower.as_ref())
.with_ctx(|_| { .await
( .with_ctx(|_| {
crate::ErrorKind::Filesystem, (
self.lower.as_ref().display().to_string(), crate::ErrorKind::Filesystem,
) lower.as_ref().display().to_string(),
})? )
.as_os_str() })?
.as_bytes(), .as_os_str()
); .as_bytes(),
);
sha.update(b"\0");
}
sha.update( sha.update(
tokio::fs::canonicalize(self.upper.as_ref()) tokio::fs::canonicalize(self.upper.as_ref())
.await .await
@@ -75,6 +82,7 @@ impl<P0: AsRef<Path> + Send + Sync, P1: AsRef<Path> + Send + Sync, P2: AsRef<Pat
.as_os_str() .as_os_str()
.as_bytes(), .as_bytes(),
); );
sha.update(b"\0");
sha.update( sha.update(
tokio::fs::canonicalize(self.work.as_ref()) tokio::fs::canonicalize(self.work.as_ref())
.await .await
@@ -87,6 +95,7 @@ impl<P0: AsRef<Path> + Send + Sync, P1: AsRef<Path> + Send + Sync, P2: AsRef<Pat
.as_os_str() .as_os_str()
.as_bytes(), .as_bytes(),
); );
sha.update(b"\0");
Ok(sha.finalize()) Ok(sha.finalize())
} }
} }
@@ -98,11 +107,20 @@ pub struct OverlayGuard<G: GenericMountGuard> {
inner_guard: MountGuard, inner_guard: MountGuard,
} }
impl<G: GenericMountGuard> OverlayGuard<G> { impl<G: GenericMountGuard> OverlayGuard<G> {
pub async fn mount(lower: G, mountpoint: impl AsRef<Path>) -> Result<Self, Error> { pub async fn mount_layers<P: AsRef<Path>>(
pre: &[P],
guard: G,
post: &[P],
mountpoint: impl AsRef<Path>,
) -> Result<Self, Error> {
let upper = TmpDir::new().await?; let upper = TmpDir::new().await?;
let inner_guard = MountGuard::mount( let inner_guard = MountGuard::mount(
&OverlayFs::new( &OverlayFs::new(
lower.path(), std::iter::empty()
.chain(pre.into_iter().map(|p| p.as_ref()))
.chain([guard.path()])
.chain(post.into_iter().map(|p| p.as_ref()))
.collect(),
upper.as_ref().join("upper"), upper.as_ref().join("upper"),
upper.as_ref().join("work"), upper.as_ref().join("work"),
), ),
@@ -111,11 +129,14 @@ impl<G: GenericMountGuard> OverlayGuard<G> {
) )
.await?; .await?;
Ok(Self { Ok(Self {
lower: Some(lower), lower: Some(guard),
upper: Some(upper), upper: Some(upper),
inner_guard, inner_guard,
}) })
} }
pub async fn mount(lower: G, mountpoint: impl AsRef<Path>) -> Result<Self, Error> {
Self::mount_layers::<&Path>(&[], lower, &[], mountpoint).await
}
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> { pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
self.inner_guard.take().unmount(delete_mountpoint).await?; self.inner_guard.take().unmount(delete_mountpoint).await?;
if let Some(lower) = self.lower.take() { if let Some(lower) = self.lower.take() {

View File

@@ -3,6 +3,7 @@ use std::path::Path;
use tracing::instrument; use tracing::instrument;
use crate::Error; use crate::Error;
use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
pub async fn is_mountpoint(path: impl AsRef<Path>) -> Result<bool, Error> { pub async fn is_mountpoint(path: impl AsRef<Path>) -> Result<bool, Error> {
@@ -56,3 +57,42 @@ pub async fn unmount<P: AsRef<Path>>(mountpoint: P, lazy: bool) -> Result<(), Er
.await?; .await?;
Ok(()) 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

@@ -4,7 +4,6 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use axum::extract::ws; use axum::extract::ws;
use const_format::formatcp;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use itertools::Itertools; use itertools::Itertools;
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};

View File

@@ -142,16 +142,16 @@ pub async fn install(
.await?, .await?,
)?; )?;
let asset = &package let (_, asset) = package
.best .best
.get(&version) .get(&version)
.and_then(|i| i.s9pks.first())
.ok_or_else(|| { .ok_or_else(|| {
Error::new( Error::new(
eyre!("{id}@{version} not found on {registry}"), eyre!("{id}@{version} not found on {registry}"),
ErrorKind::NotFound, ErrorKind::NotFound,
) )
})? })?;
.s9pk;
asset.validate(SIG_CONTEXT, asset.all_signers())?; asset.validate(SIG_CONTEXT, asset.all_signers())?;

View File

@@ -5,11 +5,13 @@ use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use futures::StreamExt; use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt};
use imbl_value::InternedString; use imbl_value::InternedString;
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{RpcRequest, RpcResponse}; use rpc_toolkit::{RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::fs::ReadDir;
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -27,7 +29,7 @@ use crate::disk::mount::util::unmount;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::service::ServiceStats; use crate::service::ServiceStats;
use crate::util::io::open_file; use crate::util::io::{open_file, write_file_owned_atomic};
use crate::util::rpc_client::UnixRpcClient; use crate::util::rpc_client::UnixRpcClient;
use crate::util::{FromStrParser, Invoke, new_guid}; use crate::util::{FromStrParser, Invoke, new_guid};
use crate::{InvalidId, PackageId}; use crate::{InvalidId, PackageId};
@@ -37,6 +39,7 @@ const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path
pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path
pub const HOST_RPC_SERVER_SOCKET: &str = "host.sock"; // must not be absolute path pub const HOST_RPC_SERVER_SOCKET: &str = "host.sock"; // must not be absolute path
const CONTAINER_DHCP_TIMEOUT: Duration = Duration::from_secs(30); const CONTAINER_DHCP_TIMEOUT: Duration = Duration::from_secs(30);
const HARDWARE_ACCELERATION_PATHS: &[&str] = &["/dev/dri", "/dev/nvidia*", "/dev/kfd"];
#[derive( #[derive(
Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS, Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS,
@@ -174,12 +177,8 @@ impl LxcContainer {
let machine_id = hex::encode(rand::random::<[u8; 16]>()); let machine_id = hex::encode(rand::random::<[u8; 16]>());
let container_dir = Path::new(LXC_CONTAINER_DIR).join(&*guid); let container_dir = Path::new(LXC_CONTAINER_DIR).join(&*guid);
tokio::fs::create_dir_all(&container_dir).await?; tokio::fs::create_dir_all(&container_dir).await?;
tokio::fs::write( let config_str = format!(include_str!("./config.template"), guid = &*guid);
container_dir.join("config"), tokio::fs::write(container_dir.join("config"), config_str).await?;
format!(include_str!("./config.template"), guid = &*guid),
)
.await?;
// TODO: append config
let rootfs_dir = container_dir.join("rootfs"); let rootfs_dir = container_dir.join("rootfs");
let rootfs = OverlayGuard::mount( let rootfs = OverlayGuard::mount(
TmpMountGuard::mount( TmpMountGuard::mount(
@@ -197,8 +196,25 @@ impl LxcContainer {
&rootfs_dir, &rootfs_dir,
) )
.await?; .await?;
tokio::fs::write(rootfs_dir.join("etc/machine-id"), format!("{machine_id}\n")).await?; Command::new("chown")
tokio::fs::write(rootfs_dir.join("etc/hostname"), format!("{guid}\n")).await?; .arg("100000:100000")
.arg(&rootfs_dir)
.invoke(ErrorKind::Filesystem)
.await?;
write_file_owned_atomic(
rootfs_dir.join("etc/machine-id"),
format!("{machine_id}\n"),
100000,
100000,
)
.await?;
write_file_owned_atomic(
rootfs_dir.join("etc/hostname"),
format!("{guid}\n"),
100000,
100000,
)
.await?;
Command::new("sed") Command::new("sed")
.arg("-i") .arg("-i")
.arg(format!("s/LXC_NAME/{guid}/g")) .arg(format!("s/LXC_NAME/{guid}/g"))
@@ -248,9 +264,13 @@ impl LxcContainer {
.arg("-d") .arg("-d")
.arg("--name") .arg("--name")
.arg(&*guid) .arg(&*guid)
.arg("-o")
.arg(format!("/run/startos/LXC_{guid}.log"))
.arg("-l")
.arg("DEBUG")
.invoke(ErrorKind::Lxc) .invoke(ErrorKind::Lxc)
.await?; .await?;
Ok(Self { let res = Self {
manager: Arc::downgrade(manager), manager: Arc::downgrade(manager),
rootfs, rootfs,
guid: Arc::new(ContainerId::try_from(&*guid)?), guid: Arc::new(ContainerId::try_from(&*guid)?),
@@ -258,7 +278,84 @@ impl LxcContainer {
config, config,
exited: false, exited: false,
log_mount, log_mount,
}) };
if res.config.hardware_acceleration {
res.handle_devices(
tokio::fs::read_dir("/dev")
.await
.with_ctx(|_| (ErrorKind::Filesystem, "readdir /dev"))?,
HARDWARE_ACCELERATION_PATHS,
)
.await?;
}
Ok(res)
}
#[cfg(not(target_os = "linux"))]
async fn handle_devices(&self, _: ReadDir, _: &[&str]) -> Result<(), Error> {
Ok(())
}
#[cfg(target_os = "linux")]
fn handle_devices<'a>(
&'a self,
mut dir: ReadDir,
matches: &'a [&'a str],
) -> BoxFuture<'a, Result<(), Error>> {
use std::os::linux::fs::MetadataExt;
use std::os::unix::fs::FileTypeExt;
async move {
while let Some(ent) = dir.next_entry().await? {
let path = ent.path();
if let Some(matches) = if matches.is_empty() {
Some(Vec::new())
} else {
let mut new_matches = Vec::new();
for mut m in matches.iter().copied() {
let could_match = if let Some(prefix) = m.strip_suffix("*") {
m = prefix;
path.to_string_lossy().starts_with(m)
} else {
path.starts_with(m)
} || Path::new(m).starts_with(&path);
if could_match {
new_matches.push(m);
}
}
if new_matches.is_empty() {
None
} else {
Some(new_matches)
}
} {
let meta = ent.metadata().await?;
let ty = meta.file_type();
if ty.is_dir() {
self.handle_devices(
tokio::fs::read_dir(&path).await.with_ctx(|_| {
(ErrorKind::Filesystem, format!("readdir {path:?}"))
})?,
&matches,
)
.await?;
} else {
let ty = if ty.is_char_device() {
'c'
} else if ty.is_block_device() {
'b'
} else {
continue;
};
let rdev = meta.st_rdev();
let major = ((rdev >> 8) & 0xfff) as u32;
let minor = ((rdev & 0xff) | ((rdev >> 12) & 0xfff00)) as u32;
self.mknod(&path, ty, major, minor).await?;
}
}
}
Ok(())
}
.boxed()
} }
pub fn rootfs_dir(&self) -> &Path { pub fn rootfs_dir(&self) -> &Path {
@@ -329,7 +426,7 @@ impl LxcContainer {
.await?; .await?;
self.rpc_bind.take().unmount().await?; self.rpc_bind.take().unmount().await?;
if let Some(log_mount) = self.log_mount.take() { if let Some(log_mount) = self.log_mount.take() {
log_mount.unmount(true).await?; log_mount.unmount(false).await?;
} }
self.rootfs.take().unmount(true).await?; self.rootfs.take().unmount(true).await?;
let rootfs_path = self.rootfs_dir(); let rootfs_path = self.rootfs_dir();
@@ -351,7 +448,10 @@ impl LxcContainer {
.invoke(ErrorKind::Lxc) .invoke(ErrorKind::Lxc)
.await?; .await?;
self.exited = true; #[allow(unused_assignments)]
{
self.exited = true;
}
Ok(()) Ok(())
} }
@@ -361,6 +461,17 @@ impl LxcContainer {
let sock_path = self.rpc_dir().join(CONTAINER_RPC_SERVER_SOCKET); let sock_path = self.rpc_dir().join(CONTAINER_RPC_SERVER_SOCKET);
while tokio::fs::metadata(&sock_path).await.is_err() { while tokio::fs::metadata(&sock_path).await.is_err() {
if timeout.map_or(false, |t| started.elapsed() > t) { if timeout.map_or(false, |t| started.elapsed() > t) {
tracing::error!(
"{:?}",
Command::new("lxc-attach")
.arg(&**self.guid)
.arg("--")
.arg("systemctl")
.arg("status")
.arg("container-runtime")
.invoke(ErrorKind::Unknown)
.await
);
return Err(Error::new( return Err(Error::new(
eyre!("timed out waiting for socket"), eyre!("timed out waiting for socket"),
ErrorKind::Timeout, ErrorKind::Timeout,
@@ -371,6 +482,88 @@ impl LxcContainer {
tracing::info!("Connected to socket in {:?}", started.elapsed()); tracing::info!("Connected to socket in {:?}", started.elapsed());
Ok(UnixRpcClient::new(sock_path)) Ok(UnixRpcClient::new(sock_path))
} }
pub async fn mknod(&self, path: &Path, ty: char, major: u32, minor: u32) -> Result<(), Error> {
if let Ok(dev_rel) = path.strip_prefix("/dev") {
let parent = dev_rel.parent();
let media_dev = self.rootfs_dir().join("media/startos/dev");
let target_path = media_dev.join(dev_rel);
if tokio::fs::metadata(&target_path).await.is_ok() {
return Ok(());
}
if let Some(parent) = parent {
let p = media_dev.join(parent);
tokio::fs::create_dir_all(&p)
.await
.with_ctx(|_| (ErrorKind::Filesystem, format!("mkdir -p {p:?}")))?;
for p in parent.ancestors() {
Command::new("chown")
.arg("100000:100000")
.arg(media_dev.join(p))
.invoke(ErrorKind::Filesystem)
.await?;
}
}
Command::new("mknod")
.arg(&target_path)
.arg(&*InternedString::from_display(&ty))
.arg(&*InternedString::from_display(&major))
.arg(&*InternedString::from_display(&minor))
.invoke(ErrorKind::Filesystem)
.await?;
Command::new("chown")
.arg("100000:100000")
.arg(&target_path)
.invoke(ErrorKind::Filesystem)
.await?;
if let Some(parent) = parent {
Command::new("lxc-attach")
.arg(&**self.guid)
.arg("--")
.arg("mkdir")
.arg("-p")
.arg(Path::new("/dev").join(parent))
.invoke(ErrorKind::Lxc)
.await?;
}
Command::new("lxc-attach")
.arg(&**self.guid)
.arg("--")
.arg("touch")
.arg(&path)
.invoke(ErrorKind::Lxc)
.await?;
Command::new("lxc-attach")
.arg(&**self.guid)
.arg("--")
.arg("mount")
.arg("--bind")
.arg(Path::new("/media/startos/dev").join(dev_rel))
.arg(&path)
.invoke(ErrorKind::Lxc)
.await?;
} else {
let target_path = self
.rootfs_dir()
.join(path.strip_prefix("/").unwrap_or(&path));
if tokio::fs::metadata(&target_path).await.is_ok() {
return Ok(());
}
Command::new("mknod")
.arg(&target_path)
.arg(&*InternedString::from_display(&ty))
.arg(&*InternedString::from_display(&major))
.arg(&*InternedString::from_display(&minor))
.invoke(ErrorKind::Filesystem)
.await?;
Command::new("chown")
.arg("100000:100000")
.arg(&target_path)
.invoke(ErrorKind::Filesystem)
.await?;
}
Ok(())
}
} }
impl Drop for LxcContainer { impl Drop for LxcContainer {
fn drop(&mut self) { fn drop(&mut self) {
@@ -414,7 +607,10 @@ impl Drop for LxcContainer {
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
pub struct LxcConfig {} pub struct LxcConfig {
pub hardware_acceleration: bool,
}
pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result<Guid, Error> { pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result<Guid, Error> {
use axum::extract::ws::Message; use axum::extract::ws::Message;

View File

@@ -15,5 +15,8 @@ fn main() {
}) { }) {
PREFER_DOCKER.set(true).ok(); PREFER_DOCKER.set(true).ok();
} }
MultiExecutable::default().enable_start_cli().execute() MultiExecutable::default()
.enable_start_cli()
.set_default("start-cli")
.execute()
} }

View File

@@ -3,5 +3,6 @@ use startos::bins::MultiExecutable;
fn main() { fn main() {
MultiExecutable::default() MultiExecutable::default()
.enable_start_container() .enable_start_container()
.set_default("start-container")
.execute() .execute()
} }

View File

@@ -151,103 +151,112 @@ where
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> { ) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
self.in_progress.mutate(|in_progress| { self.in_progress.mutate(|in_progress| {
loop { // First, check if any in-progress handshakes have completed
if !in_progress.is_empty() { if !in_progress.is_empty() {
if let Poll::Ready(Some((handler, res))) = in_progress.poll_next_unpin(cx) { if let Poll::Ready(Some((handler, res))) = in_progress.poll_next_unpin(cx) {
if let Some(res) = res.transpose() { if let Some(res) = res.transpose() {
self.tls_handler = handler; self.tls_handler = handler;
return Poll::Ready(res); return Poll::Ready(res);
}
continue;
} }
// Connection was rejected (preprocess returned None).
// Yield to the runtime to avoid busy-looping, but wake
// immediately to continue processing.
cx.waker().wake_by_ref();
return Poll::Pending;
} }
}
let (metadata, stream) = ready!(self.accept.poll_accept(cx)?); // Try to accept a new connection
let mut tls_handler = self.tls_handler.clone(); let (metadata, stream) = ready!(self.accept.poll_accept(cx)?);
let mut fut = async move { let mut tls_handler = self.tls_handler.clone();
let res = async { let mut fut = async move {
let mut acceptor = LazyConfigAcceptor::new( let res = async {
Acceptor::default(), let mut acceptor = LazyConfigAcceptor::new(
BackTrackingIO::new(stream), Acceptor::default(),
); BackTrackingIO::new(stream),
let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> = );
match (&mut acceptor).await { let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> =
Ok(a) => a, match (&mut acceptor).await {
Err(e) => { Ok(a) => a,
let mut stream = Err(e) => {
acceptor.take_io().or_not_found("acceptor io")?; let mut stream =
let (_, buf) = stream.rewind(); acceptor.take_io().or_not_found("acceptor io")?;
if std::str::from_utf8(buf) let (_, buf) = stream.rewind();
.ok() if std::str::from_utf8(buf)
.and_then(|buf| { .ok()
buf.lines() .and_then(|buf| {
.map(|l| l.trim()) buf.lines()
.filter(|l| !l.is_empty()) .map(|l| l.trim())
.next() .filter(|l| !l.is_empty())
}) .next()
.map_or(false, |buf| { })
regex::Regex::new("[A-Z]+ (.+) HTTP/1") .map_or(false, |buf| {
.unwrap() regex::Regex::new("[A-Z]+ (.+) HTTP/1")
.is_match(buf) .unwrap()
}) .is_match(buf)
{ })
handle_http_on_https(stream).await.log_err(); {
handle_http_on_https(stream).await.log_err();
return Ok(None); return Ok(None);
} else { } else {
return Err(e).with_kind(ErrorKind::Network); return Err(e).with_kind(ErrorKind::Network);
}
} }
}; }
let hello = mid.client_hello(); };
if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await { let hello = mid.client_hello();
let buffered = mid.io.stop_buffering(); if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await {
mid.io let buffered = mid.io.stop_buffering();
.write_all(&buffered) mid.io
.await .write_all(&buffered)
.with_kind(ErrorKind::Network)?; .await
return Ok(match mid.into_stream(Arc::new(cfg)).await { .with_kind(ErrorKind::Network)?;
Ok(stream) => { return Ok(match mid.into_stream(Arc::new(cfg)).await {
let s = stream.get_ref().1; Ok(stream) => {
Some(( let s = stream.get_ref().1;
TlsMetadata { Some((
inner: metadata, TlsMetadata {
tls_info: TlsHandshakeInfo { inner: metadata,
sni: s.server_name().map(InternedString::intern), tls_info: TlsHandshakeInfo {
alpn: s sni: s.server_name().map(InternedString::intern),
.alpn_protocol() alpn: s
.map(|a| MaybeUtf8String(a.to_vec())), .alpn_protocol()
}, .map(|a| MaybeUtf8String(a.to_vec())),
}, },
Box::pin(stream) as AcceptStream, },
)) Box::pin(stream) as AcceptStream,
} ))
Err(e) => { }
tracing::trace!("Error completing TLS handshake: {e}"); Err(e) => {
tracing::trace!("{e:?}"); tracing::trace!("Error completing TLS handshake: {e}");
None tracing::trace!("{e:?}");
} None
}); }
} });
}
Ok(None) Ok(None)
}
.await;
(tls_handler, res)
} }
.boxed(); .await;
match fut.poll_unpin(cx) { (tls_handler, res)
Poll::Pending => { }
in_progress.push(fut); .boxed();
return Poll::Pending; match fut.poll_unpin(cx) {
Poll::Pending => {
in_progress.push(fut);
Poll::Pending
}
Poll::Ready((handler, res)) => {
if let Some(res) = res.transpose() {
self.tls_handler = handler;
return Poll::Ready(res);
} }
Poll::Ready((handler, res)) => { // Connection was rejected (preprocess returned None).
if let Some(res) = res.transpose() { // Yield to the runtime to avoid busy-looping, but wake
self.tls_handler = handler; // immediately to continue processing.
return Poll::Ready(res); cx.waker().wake_by_ref();
} Poll::Pending
} }
};
} }
}) })
} }

View File

@@ -280,8 +280,11 @@ pub async fn execute<C: Context>(
let lower = TmpMountGuard::mount(&BlockDev::new(&image_path), MountType::ReadOnly).await?; let lower = TmpMountGuard::mount(&BlockDev::new(&image_path), MountType::ReadOnly).await?;
let work = config_path.join("work"); let work = config_path.join("work");
let upper = config_path.join("overlay"); let upper = config_path.join("overlay");
let overlay = let overlay = TmpMountGuard::mount(
TmpMountGuard::mount(&OverlayFs::new(&lower.path(), &upper, &work), ReadWrite).await?; &OverlayFs::new(vec![lower.path()], &upper, &work),
ReadWrite,
)
.await?;
let boot = MountGuard::mount( let boot = MountGuard::mount(
&BlockDev::new(&part_info.boot), &BlockDev::new(&part_info.boot),

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use reqwest::Client; use reqwest::{Client, Response};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use ts_rs::TS; use ts_rs::TS;
@@ -21,14 +21,14 @@ use crate::sign::{AnySignature, AnyVerifyingKey};
use crate::upload::UploadingFile; use crate::upload::UploadingFile;
use crate::util::future::NonDetachingJoinHandle; use crate::util::future::NonDetachingJoinHandle;
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct RegistryAsset<Commitment> { pub struct RegistryAsset<Commitment> {
#[ts(type = "string")] #[ts(type = "string")]
pub published_at: DateTime<Utc>, pub published_at: DateTime<Utc>,
#[ts(type = "string")] #[ts(type = "string[]")]
pub url: Url, pub urls: Vec<Url>,
pub commitment: Commitment, pub commitment: Commitment,
pub signatures: HashMap<AnyVerifyingKey, AnySignature>, pub signatures: HashMap<AnyVerifyingKey, AnySignature>,
} }
@@ -42,6 +42,48 @@ impl<Commitment> RegistryAsset<Commitment> {
.collect(), .collect(),
) )
} }
pub async fn load_http_source(&self, client: Client) -> Result<HttpSource, Error> {
for url in &self.urls {
if let Ok(source) = HttpSource::new(client.clone(), url.clone()).await {
return Ok(source);
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
ErrorKind::Network,
))
}
pub async fn load_buffered_http_source(
&self,
client: Client,
progress: PhaseProgressTrackerHandle,
) -> Result<BufferedHttpSource, Error> {
for url in &self.urls {
if let Ok(response) = client.get(url.clone()).send().await {
return BufferedHttpSource::from_response(response, progress).await;
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
ErrorKind::Network,
))
}
pub async fn load_buffered_http_source_with_path(
&self,
path: impl AsRef<Path>,
client: Client,
progress: PhaseProgressTrackerHandle,
) -> Result<BufferedHttpSource, Error> {
for url in &self.urls {
if let Ok(response) = client.get(url.clone()).send().await {
return BufferedHttpSource::from_response_with_path(path, response, progress).await;
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
ErrorKind::Network,
))
}
} }
impl<Commitment: Digestable> RegistryAsset<Commitment> { impl<Commitment: Digestable> RegistryAsset<Commitment> {
pub fn validate(&self, context: &str, mut accept: AcceptSigners) -> Result<&Commitment, Error> { pub fn validate(&self, context: &str, mut accept: AcceptSigners) -> Result<&Commitment, Error> {
@@ -59,7 +101,7 @@ impl<C: for<'a> Commitment<&'a HttpSource>> RegistryAsset<C> {
dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized),
) -> Result<(), Error> { ) -> Result<(), Error> {
self.commitment self.commitment
.copy_to(&HttpSource::new(client, self.url.clone()).await?, dst) .copy_to(&self.load_http_source(client).await?, dst)
.await .await
} }
} }
@@ -69,7 +111,7 @@ impl RegistryAsset<MerkleArchiveCommitment> {
client: Client, client: Client,
) -> Result<S9pk<Section<Arc<HttpSource>>>, Error> { ) -> Result<S9pk<Section<Arc<HttpSource>>>, Error> {
S9pk::deserialize( S9pk::deserialize(
&Arc::new(HttpSource::new(client, self.url.clone()).await?), &Arc::new(self.load_http_source(client).await?),
Some(&self.commitment), Some(&self.commitment),
) )
.await .await
@@ -80,7 +122,7 @@ impl RegistryAsset<MerkleArchiveCommitment> {
progress: PhaseProgressTrackerHandle, progress: PhaseProgressTrackerHandle,
) -> Result<S9pk<Section<Arc<BufferedHttpSource>>>, Error> { ) -> Result<S9pk<Section<Arc<BufferedHttpSource>>>, Error> {
S9pk::deserialize( S9pk::deserialize(
&Arc::new(BufferedHttpSource::new(client, self.url.clone(), progress).await?), &Arc::new(self.load_buffered_http_source(client, progress).await?),
Some(&self.commitment), Some(&self.commitment),
) )
.await .await
@@ -98,7 +140,8 @@ impl RegistryAsset<MerkleArchiveCommitment> {
Error, Error,
> { > {
let source = Arc::new( let source = Arc::new(
BufferedHttpSource::with_path(path, client, self.url.clone(), progress).await?, self.load_buffered_http_source_with_path(path, client, progress)
.await?,
); );
Ok(( Ok((
S9pk::deserialize(&source, Some(&self.commitment)).await?, S9pk::deserialize(&source, Some(&self.commitment)).await?,
@@ -112,26 +155,30 @@ pub struct BufferedHttpSource {
file: UploadingFile, file: UploadingFile,
} }
impl BufferedHttpSource { impl BufferedHttpSource {
pub async fn with_path(
path: impl AsRef<Path>,
client: Client,
url: Url,
progress: PhaseProgressTrackerHandle,
) -> Result<Self, Error> {
let (mut handle, file) = UploadingFile::with_path(path, progress).await?;
let response = client.get(url).send().await?;
Ok(Self {
_download: tokio::spawn(async move { handle.download(response).await }).into(),
file,
})
}
pub async fn new( pub async fn new(
client: Client, client: Client,
url: Url, url: Url,
progress: PhaseProgressTrackerHandle, progress: PhaseProgressTrackerHandle,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let (mut handle, file) = UploadingFile::new(progress).await?;
let response = client.get(url).send().await?; let response = client.get(url).send().await?;
Self::from_response(response, progress).await
}
pub async fn from_response(
response: Response,
progress: PhaseProgressTrackerHandle,
) -> Result<Self, Error> {
let (mut handle, file) = UploadingFile::new(progress).await?;
Ok(Self {
_download: tokio::spawn(async move { handle.download(response).await }).into(),
file,
})
}
pub async fn from_response_with_path(
path: impl AsRef<Path>,
response: Response,
progress: PhaseProgressTrackerHandle,
) -> Result<Self, Error> {
let (mut handle, file) = UploadingFile::with_path(path, progress).await?;
Ok(Self { Ok(Self {
_download: tokio::spawn(async move { handle.download(response).await }).into(), _download: tokio::spawn(async move { handle.download(response).await }).into(),
file, file,

View File

@@ -7,6 +7,7 @@ use chrono::Utc;
use clap::Parser; use clap::Parser;
use cookie::{Cookie, Expiration, SameSite}; use cookie::{Cookie, Expiration, SameSite};
use http::HeaderMap; use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::PatchDb; use patch_db::PatchDb;
use patch_db::json_ptr::ROOT; use patch_db::json_ptr::ROOT;
@@ -171,6 +172,7 @@ impl CallRemote<RegistryContext> for CliContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
_: Empty, _: Empty,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
@@ -240,14 +242,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value, params: Value,
RegistryUrlParams { mut registry }: RegistryUrlParams, RegistryUrlParams { mut registry }: RegistryUrlParams,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( let mut device_info = None;
DEVICE_INFO_HEADER, if metadata
DeviceInfo::load(self).await?.to_header_value(), .get("get_device_info")
); .and_then(|m| m.as_bool())
.unwrap_or(false)
{
let di = DeviceInfo::load(self).await?;
headers.insert(DEVICE_INFO_HEADER, di.to_header_value());
device_info = Some(di);
}
registry registry
.path_segments_mut() .path_segments_mut()
@@ -258,15 +267,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
method = method.strip_prefix("registry.").unwrap_or(method); method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from); let sig_context = registry.host_str().map(InternedString::from);
crate::middleware::auth::signature::call_remote( let mut res = crate::middleware::auth::signature::call_remote(
self, self,
registry, registry,
headers, headers,
sig_context.as_deref(), sig_context.as_deref(),
method, method,
params, params.clone(),
) )
.await .await?;
if let Some(device_info) = device_info {
device_info.filter_for_hardware(method, params, &mut res)?;
}
Ok(res)
} }
} }

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::identity;
use std::ops::Deref; use std::ops::Deref;
use axum::extract::Request; use axum::extract::Request;
@@ -7,6 +6,8 @@ use axum::response::Response;
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use http::HeaderValue; use http::HeaderValue;
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::ModelExt;
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
@@ -15,8 +16,13 @@ use url::Url;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::prelude::*; use crate::prelude::*;
use crate::registry::context::RegistryContext; 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::VersionString;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor}; use crate::util::lshw::LshwDevice;
use crate::version::VersionT; use crate::version::VersionT;
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info"; pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
@@ -25,13 +31,13 @@ pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DeviceInfo { pub struct DeviceInfo {
pub os: OsInfo, pub os: OsInfo,
pub hardware: HardwareInfo, pub hardware: Option<HardwareInfo>,
} }
impl DeviceInfo { impl DeviceInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
Ok(Self { Ok(Self {
os: OsInfo::from(ctx), os: OsInfo::from(ctx),
hardware: HardwareInfo::load(ctx).await?, hardware: Some(HardwareInfo::load(ctx).await?),
}) })
} }
} }
@@ -41,21 +47,13 @@ impl DeviceInfo {
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("os.version", &self.os.version.to_string()) .append_pair("os.version", &self.os.version.to_string())
.append_pair("os.compat", &self.os.compat.to_string()) .append_pair("os.compat", &self.os.compat.to_string())
.append_pair("os.platform", &*self.os.platform) .append_pair("os.platform", &*self.os.platform);
.append_pair("hardware.arch", &*self.hardware.arch)
.append_pair("hardware.ram", &self.hardware.ram.to_string());
for device in &self.hardware.devices {
url.query_pairs_mut().append_pair(
&format!("hardware.device.{}", device.class()),
device.product(),
);
}
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
} }
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> { pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
Ok(Self { Ok(Self {
os: OsInfo { os: OsInfo {
version: query version: query
@@ -69,35 +67,120 @@ impl DeviceInfo {
.deref() .deref()
.into(), .into(),
}, },
hardware: HardwareInfo { hardware: has_hw_info
arch: query .then(|| {
.get("hardware.arch") Ok::<_, Error>(HardwareInfo {
.or_not_found("hardware.arch")? arch: query
.parse()?, .get("hardware.arch")
ram: query .or_not_found("hardware.arch")?
.get("hardware.ram") .parse()?,
.or_not_found("hardware.ram")? ram: query
.parse()?, .get("hardware.ram")
devices: identity(query) .or_not_found("hardware.ram")?
.split_off("hardware.device.") .parse()?,
.into_iter() devices: None,
.filter_map(|(k, v)| match k.strip_prefix("hardware.device.") {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor {
product: v.into_owned(),
})),
Some("display") => Some(LshwDevice::Display(LshwDisplay {
product: v.into_owned(),
})),
Some(class) => {
tracing::warn!("unknown device class: {class}");
None
}
_ => None,
}) })
.collect(), })
}, .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)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -127,7 +210,7 @@ pub struct HardwareInfo {
pub arch: InternedString, pub arch: InternedString,
#[ts(type = "number")] #[ts(type = "number")]
pub ram: u64, pub ram: u64,
pub devices: Vec<LshwDevice>, pub devices: Option<Vec<LshwDevice>>,
} }
impl HardwareInfo { impl HardwareInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
@@ -135,7 +218,7 @@ impl HardwareInfo {
Ok(Self { Ok(Self {
arch: s.as_arch().de()?, arch: s.as_arch().de()?,
ram: s.as_ram().de()?, ram: s.as_ram().de()?,
devices: s.as_devices().de()?, devices: Some(s.as_devices().de()?),
}) })
} }
} }
@@ -148,11 +231,17 @@ pub struct Metadata {
#[derive(Clone)] #[derive(Clone)]
pub struct DeviceInfoMiddleware { pub struct DeviceInfoMiddleware {
device_info: Option<HeaderValue>, device_info_header: Option<HeaderValue>,
device_info: Option<DeviceInfo>,
req: Option<RpcRequest>,
} }
impl DeviceInfoMiddleware { impl DeviceInfoMiddleware {
pub fn new() -> Self { pub fn new() -> Self {
Self { device_info: None } Self {
device_info_header: None,
device_info: None,
req: None,
}
} }
} }
@@ -163,7 +252,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
_: &RegistryContext, _: &RegistryContext,
request: &mut Request, request: &mut Request,
) -> Result<(), Response> { ) -> Result<(), Response> {
self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER); self.device_info_header = request.headers_mut().remove(DEVICE_INFO_HEADER);
Ok(()) Ok(())
} }
async fn process_rpc_request( async fn process_rpc_request(
@@ -174,9 +263,11 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
) -> Result<(), RpcResponse> { ) -> Result<(), RpcResponse> {
async move { async move {
if metadata.get_device_info { if metadata.get_device_info {
if let Some(device_info) = &self.device_info { if let Some(device_info) = &self.device_info_header {
request.params["__DeviceInfo_device_info"] = let device_info = DeviceInfo::from_header_value(device_info)?;
to_value(&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());
} }
} }
@@ -185,4 +276,19 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
.await .await
.map_err(|e| RpcResponse::from_result(Err(e))) .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

@@ -5,9 +5,6 @@ use crate::prelude::*;
pub struct PackageSignerScopeMigration; pub struct PackageSignerScopeMigration;
impl RegistryMigration for PackageSignerScopeMigration { impl RegistryMigration for PackageSignerScopeMigration {
fn name(&self) -> &'static str {
"PackageSignerScopeMigration"
}
fn action(&self, db: &mut Value) -> Result<(), Error> { fn action(&self, db: &mut Value) -> Result<(), Error> {
for (_, info) in db["index"]["package"]["packages"] for (_, info) in db["index"]["package"]["packages"]
.as_object_mut() .as_object_mut()

View File

@@ -0,0 +1,35 @@
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

@@ -4,22 +4,29 @@ use crate::prelude::*;
use crate::registry::RegistryDatabase; use crate::registry::RegistryDatabase;
mod m_00_package_signer_scope; mod m_00_package_signer_scope;
mod m_01_registry_asset_array;
pub trait RegistryMigration { pub trait RegistryMigration {
fn name(&self) -> &'static str; fn name(&self) -> &'static str {
let val = std::any::type_name_of_val(self);
val.rsplit_once("::").map_or(val, |v| v.1)
}
fn action(&self, db: &mut Value) -> Result<(), Error>; fn action(&self, db: &mut Value) -> Result<(), Error>;
} }
pub const MIGRATIONS: &[&dyn RegistryMigration] = pub const MIGRATIONS: &[&dyn RegistryMigration] = &[
&[&m_00_package_signer_scope::PackageSignerScopeMigration]; &m_00_package_signer_scope::PackageSignerScopeMigration,
&m_01_registry_asset_array::RegistryAssetArray,
];
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn run_migrations(db: &mut Model<RegistryDatabase>) -> Result<(), Error> { pub fn run_migrations(db: &mut Model<RegistryDatabase>) -> Result<(), Error> {
let mut migrations = db.as_migrations().de().unwrap_or_default(); let mut migrations = db.as_migrations().de().unwrap_or_default();
for migration in MIGRATIONS { for migration in MIGRATIONS {
if !migrations.contains(migration.name()) { let name = migration.name();
if !migrations.contains(name) {
migration.action(ModelExt::as_value_mut(db))?; migration.action(ModelExt::as_value_mut(db))?;
migrations.insert(migration.name().into()); migrations.insert(name.into());
} }
} }
let mut db_deser = db.de()?; let mut db_deser = db.de()?;

View File

@@ -133,7 +133,7 @@ async fn add_asset(
.upsert(&platform, || { .upsert(&platform, || {
Ok(RegistryAsset { Ok(RegistryAsset {
published_at: Utc::now(), published_at: Utc::now(),
url, urls: vec![url.clone()],
commitment: commitment.clone(), commitment: commitment.clone(),
signatures: HashMap::new(), signatures: HashMap::new(),
}) })
@@ -146,6 +146,9 @@ async fn add_asset(
)) ))
} else { } else {
s.signatures.insert(signer, signature); s.signatures.insert(signer, signature);
if !s.urls.contains(&url) {
s.urls.push(url);
}
Ok(()) Ok(())
} }
})?; })?;

View File

@@ -187,7 +187,8 @@ pub async fn get_version(
platform, platform,
device_info, device_info,
}: GetOsVersionParams, }: GetOsVersionParams,
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> { ) -> Result<Value, Error> // BTreeMap<Version, OsVersionInfo>
{
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone())); let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone())); let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
@@ -202,33 +203,63 @@ pub async fn get_version(
.with_kind(ErrorKind::Database)?; .with_kind(ErrorKind::Database)?;
} }
let target = target.unwrap_or(VersionRange::Any); let target = target.unwrap_or(VersionRange::Any);
ctx.db let mut res = to_value::<BTreeMap<Version, OsVersionInfo>>(
.peek() &ctx.db
.await .peek()
.into_index() .await
.into_os() .into_index()
.into_versions() .into_os()
.into_entries()? .into_versions()
.into_iter() .into_entries()?
.map(|(v, i)| i.de().map(|i| (v, i))) .into_iter()
.filter_ok(|(version, info)| { .map(|(v, i)| i.de().map(|i| (v, i)))
platform .filter_ok(|(version, info)| {
.as_ref() platform
.map_or(true, |p| info.squashfs.contains_key(p))
&& version.satisfies(&target)
&& source
.as_ref() .as_ref()
.map_or(true, |s| s.satisfies(&info.source_version)) .map_or(true, |p| info.squashfs.contains_key(p))
}) && version.satisfies(&target)
.collect() && source
.as_ref()
.map_or(true, |s| s.satisfies(&info.source_version))
})
.collect::<Result<_, _>>()?,
)?;
// TODO: remove
if device_info.map_or(false, |d| {
"0.4.0-alpha.17"
.parse::<Version>()
.map_or(false, |v| d.os.version <= v)
}) {
for (_, v) in res
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
for asset_ty in ["iso", "squashfs", "img"] {
for (_, v) in v[asset_ty]
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
v["url"] = v["urls"][0].clone();
}
}
}
}
Ok(res)
} }
pub fn display_version_info<T>( pub fn display_version_info<T>(
params: WithIoFormat<T>, params: WithIoFormat<T>,
info: BTreeMap<Version, OsVersionInfo>, info: Value, // BTreeMap<Version, OsVersionInfo>,
) -> Result<(), Error> { ) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
let info = from_value::<BTreeMap<Version, OsVersionInfo>>(info)?;
if let Some(format) = params.format { if let Some(format) = params.format {
return display_serializable(format, info); return display_serializable(format, info);
} }

View File

@@ -12,12 +12,11 @@ use url::Url;
use crate::PackageId; use crate::PackageId;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, ProgressTrackerWriter, ProgressUnits}; use crate::progress::FullProgressTracker;
use crate::registry::asset::BufferedHttpSource; use crate::registry::asset::BufferedHttpSource;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::package::index::PackageVersionInfo; use crate::registry::package::index::PackageVersionInfo;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
@@ -25,13 +24,14 @@ use crate::sign::ed25519::Ed25519;
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
use crate::util::VersionString; use crate::util::VersionString;
use crate::util::io::TrackingIO; use crate::util::io::TrackingIO;
use crate::util::serde::Base64;
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct AddPackageParams { pub struct AddPackageParams {
#[ts(type = "string")] #[ts(type = "string[]")]
pub url: Url, pub urls: Vec<Url>,
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__Auth_signer")] #[serde(rename = "__Auth_signer")]
pub uploader: AnyVerifyingKey, pub uploader: AnyVerifyingKey,
@@ -42,7 +42,7 @@ pub struct AddPackageParams {
pub async fn add_package( pub async fn add_package(
ctx: RegistryContext, ctx: RegistryContext,
AddPackageParams { AddPackageParams {
url, urls,
uploader, uploader,
commitment, commitment,
signature, signature,
@@ -53,17 +53,35 @@ pub async fn add_package(
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?; .verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?; 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!("must specify at least 1 url"),
ErrorKind::InvalidRequest,
));
};
let s9pk = S9pk::deserialize( let s9pk = S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
Some(&commitment), Some(&commitment),
) )
.await?; .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 manifest = s9pk.as_manifest();
let mut info = PackageVersionInfo::from_s9pk(&s9pk, url).await?; let mut info = PackageVersionInfo::from_s9pk(&s9pk, urls).await?;
if !info.s9pk.signatures.contains_key(&uploader) { for (_, s9pk) in &mut info.s9pks {
info.s9pk.signatures.insert(uploader.clone(), signature); if !s9pk.signatures.contains_key(&uploader) && s9pk.commitment == commitment {
s9pk.signatures.insert(uploader.clone(), signature.clone());
}
} }
ctx.db ctx.db
@@ -85,7 +103,12 @@ pub async fn add_package(
.as_package_mut() .as_package_mut()
.as_packages_mut() .as_packages_mut()
.upsert(&manifest.id, || Ok(Default::default()))?; .upsert(&manifest.id, || Ok(Default::default()))?;
package.as_versions_mut().insert(&manifest.version, &info)?; 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(()) Ok(())
} else { } else {
@@ -101,7 +124,10 @@ pub async fn add_package(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CliAddPackageParams { pub struct CliAddPackageParams {
pub file: PathBuf, pub file: PathBuf,
pub url: Url, #[arg(long)]
pub url: Vec<Url>,
#[arg(long)]
pub no_verify: bool,
} }
pub async fn cli_add_package( pub async fn cli_add_package(
@@ -109,7 +135,12 @@ pub async fn cli_add_package(
context: ctx, context: ctx,
parent_method, parent_method,
method, method,
params: CliAddPackageParams { file, url }, params:
CliAddPackageParams {
file,
url,
no_verify,
},
.. ..
}: HandlerArgs<CliContext, CliAddPackageParams>, }: HandlerArgs<CliContext, CliAddPackageParams>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -117,7 +148,19 @@ pub async fn cli_add_package(
let progress = FullProgressTracker::new(); let progress = FullProgressTracker::new();
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1)); let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100)); 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( let mut index_phase = progress.add_phase(
InternedString::intern("Adding File to Registry Index"), InternedString::intern("Adding File to Registry Index"),
Some(1), Some(1),
@@ -131,11 +174,240 @@ pub async fn cli_add_package(
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
sign_phase.complete(); sign_phase.complete();
verify_phase.start(); for (url, mut phase) in verify {
let source = BufferedHttpSource::new(ctx.client.clone(), url.clone(), verify_phase).await?; phase.start();
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?; let source = BufferedHttpSource::new(ctx.client.clone(), url, phase).await?;
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true) let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
.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!({
"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 {
pub id: PackageId,
pub version: VersionString,
#[arg(long)]
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!("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!("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!("UNAUTHORIZED"), ErrorKind::Authorization))
}
})
.await
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddMirrorParams {
pub file: PathBuf,
pub url: Url,
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(); index_phase.start();
ctx.call_remote::<RegistryContext>( ctx.call_remote::<RegistryContext>(
@@ -159,22 +431,26 @@ pub async fn cli_add_package(
#[derive(Debug, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct RemovePackageParams { pub struct RemoveMirrorParams {
pub id: PackageId, pub id: PackageId,
pub version: VersionString, pub version: VersionString,
#[arg(long)]
#[ts(type = "string")]
pub url: Url,
#[ts(skip)] #[ts(skip)]
#[arg(skip)] #[arg(skip)]
#[serde(rename = "__Auth_signer")] #[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>, pub signer: Option<AnyVerifyingKey>,
} }
pub async fn remove_package( pub async fn remove_mirror(
ctx: RegistryContext, ctx: RegistryContext,
RemovePackageParams { RemoveMirrorParams {
id, id,
version, version,
url,
signer, signer,
}: RemovePackageParams, }: RemoveMirrorParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let signer = let signer =
@@ -200,8 +476,20 @@ pub async fn remove_package(
.as_package_mut() .as_package_mut()
.as_packages_mut() .as_packages_mut()
.as_idx_mut(&id) .as_idx_mut(&id)
.and_then(|p| p.as_versions_mut().as_idx_mut(&version))
{ {
package.as_versions_mut().remove(&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!("cannot remove last mirror from an s9pk"),
ErrorKind::InvalidRequest,
))
} else {
Ok(())
}
})?;
} }
Ok(()) Ok(())
} else { } else {

View File

@@ -20,12 +20,12 @@ use crate::s9pk::v2::SIG_CONTEXT;
use crate::util::VersionString; use crate::util::VersionString;
use crate::util::io::{TrackingIO, to_tmp_path}; use crate::util::io::{TrackingIO, to_tmp_path};
use crate::util::serde::{WithIoFormat, display_serializable}; use crate::util::serde::{WithIoFormat, display_serializable};
use crate::util::tui::choose; use crate::util::tui::{choose, choose_custom_display};
#[derive( #[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
)] )]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "kebab-case")]
#[ts(export)] #[ts(export)]
pub enum PackageDetailLevel { pub enum PackageDetailLevel {
None, None,
@@ -45,10 +45,11 @@ pub struct PackageInfoShort {
pub release_notes: String, pub release_notes: String,
} }
#[derive(Debug, Deserialize, Serialize, TS, Parser)] #[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageParams { pub struct GetPackageParams {
pub id: Option<PackageId>, pub id: Option<PackageId>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
@@ -60,14 +61,14 @@ pub struct GetPackageParams {
#[arg(skip)] #[arg(skip)]
#[serde(rename = "__DeviceInfo_device_info")] #[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>, pub device_info: Option<DeviceInfo>,
#[serde(default)]
#[arg(default_value = "none")] #[arg(default_value = "none")]
pub other_versions: PackageDetailLevel, pub other_versions: Option<PackageDetailLevel>,
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponse { pub struct GetPackageResponse {
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>, pub categories: BTreeSet<InternedString>,
@@ -108,9 +109,10 @@ impl GetPackageResponse {
} }
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponseFull { pub struct GetPackageResponseFull {
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>, pub categories: BTreeSet<InternedString>,
@@ -134,15 +136,15 @@ impl GetPackageResponseFull {
pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>; pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>;
pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>; pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>;
fn get_matching_models<'a>( fn get_matching_models(
db: &'a Model<PackageIndex>, db: &Model<PackageIndex>,
GetPackageParams { GetPackageParams {
id, id,
source_version, source_version,
device_info, device_info,
.. ..
}: &GetPackageParams, }: &GetPackageParams,
) -> Result<Vec<(PackageId, ExtendedVersion, &'a Model<PackageVersionInfo>)>, Error> { ) -> Result<Vec<(PackageId, ExtendedVersion, Model<PackageVersionInfo>)>, Error> {
if let Some(id) = id { if let Some(id) = id {
if let Some(pkg) = db.as_packages().as_idx(id) { if let Some(pkg) = db.as_packages().as_idx(id) {
vec![(id.clone(), pkg)] vec![(id.clone(), pkg)]
@@ -168,11 +170,17 @@ fn get_matching_models<'a>(
.unwrap_or(VersionRange::any()), .unwrap_or(VersionRange::any()),
), ),
) )
})? && device_info })? {
.as_ref() let mut info = info.clone();
.map_or(Ok(true), |device_info| info.works_for_device(device_info))? if let Some(device_info) = &device_info {
{ if info.for_device(device_info)? {
Some((k.clone(), ExtendedVersion::from(v), info)) Some((k.clone(), ExtendedVersion::from(v), info))
} else {
None
}
} else {
Some((k.clone(), ExtendedVersion::from(v), info))
}
} else { } else {
None None
}, },
@@ -186,12 +194,10 @@ fn get_matching_models<'a>(
} }
pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> { pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> {
use patch_db::ModelExt;
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let mut best: BTreeMap<PackageId, BTreeMap<VersionString, &Model<PackageVersionInfo>>> = let mut best: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
Default::default(); Default::default();
let mut other: BTreeMap<PackageId, BTreeMap<VersionString, &Model<PackageVersionInfo>>> = let mut other: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
Default::default(); Default::default();
for (id, version, info) in get_matching_models(&peek.as_index().as_package(), &params)? { for (id, version, info) in get_matching_models(&peek.as_index().as_package(), &params)? {
let package_best = best.entry(id.clone()).or_default(); let package_best = best.entry(id.clone()).or_default();
@@ -217,23 +223,23 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
package_other.insert(version.into(), info); package_other.insert(version.into(), info);
} }
} }
if let Some(id) = params.id { if let Some(id) = &params.id {
let categories = peek let categories = peek
.as_index() .as_index()
.as_package() .as_package()
.as_packages() .as_packages()
.as_idx(&id) .as_idx(id)
.map(|p| p.as_categories().de()) .map(|p| p.as_categories().de())
.transpose()? .transpose()?
.unwrap_or_default(); .unwrap_or_default();
let best = best let best: BTreeMap<VersionString, PackageVersionInfo> = best
.remove(&id) .remove(id)
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?; .try_collect()?;
let other = other.remove(&id).unwrap_or_default(); let other = other.remove(id).unwrap_or_default();
match params.other_versions { match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value(&GetPackageResponse { PackageDetailLevel::None => to_value(&GetPackageResponse {
categories, categories,
best, best,
@@ -245,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
other_versions: Some( other_versions: Some(
other other
.into_iter() .into_iter()
.map(|(k, v)| from_value(v.as_value().clone()).map(|v| (k, v))) .map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
.try_collect()?, .try_collect()?,
), ),
}), }),
@@ -254,12 +260,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
best, best,
other_versions: other other_versions: other
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?, .try_collect()?,
}), }),
} }
} else { } else {
match params.other_versions { match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value( PackageDetailLevel::None => to_value(
&best &best
.into_iter() .into_iter()
@@ -278,7 +284,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?, .try_collect()?,
other_versions: None, other_versions: None,
}, },
@@ -305,14 +311,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?, .try_collect()?,
other_versions: Some( other_versions: Some(
other other
.into_iter() .into_iter()
.map(|(k, v)| { .map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
from_value(v.as_value().clone()).map(|v| (k, v))
})
.try_collect()?, .try_collect()?,
), ),
}, },
@@ -339,11 +343,11 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?, .try_collect()?,
other_versions: other other_versions: other
.into_iter() .into_iter()
.map(|(k, v)| v.de().map(|v| (k, v))) .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.try_collect()?, .try_collect()?,
}, },
)) ))
@@ -363,7 +367,7 @@ pub fn display_package_info(
} }
if let Some(_) = params.rest.id { if let Some(_) = params.rest.id {
if params.rest.other_versions == PackageDetailLevel::Full { if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for table in from_value::<GetPackageResponseFull>(info)?.tables() { for table in from_value::<GetPackageResponseFull>(info)?.tables() {
table.print_tty(false)?; table.print_tty(false)?;
println!(); println!();
@@ -375,7 +379,7 @@ pub fn display_package_info(
} }
} }
} else { } else {
if params.rest.other_versions == PackageDetailLevel::Full { if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for (_, package) in from_value::<GetPackagesResponseFull>(info)? { for (_, package) in from_value::<GetPackagesResponseFull>(info)? {
for table in package.tables() { for table in package.tables() {
table.print_tty(false)?; table.print_tty(false)?;
@@ -431,7 +435,9 @@ pub async fn cli_download(
) )
.await?, .await?,
)?; )?;
let PackageVersionInfo { s9pk, .. } = match res.best.len() { let PackageVersionInfo {
s9pks: mut s9pk, ..
} = match res.best.len() {
0 => { 0 => {
return Err(Error::new( return Err(Error::new(
eyre!( eyre!(
@@ -452,6 +458,75 @@ pub async fn cli_download(
res.best.remove(version).unwrap() res.best.remove(version).unwrap()
} }
}; };
let s9pk = match s9pk.len() {
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
),
ErrorKind::NotFound,
));
}
1 => s9pk.pop().unwrap().1,
_ => {
let (_, asset) = choose_custom_display(
&format!(concat!(
"Multiple packages with different hardware requirements found. ",
"Choose a file to download:"
)),
&s9pk,
|(hw, _)| {
use std::fmt::Write;
let mut res = String::new();
if let Some(arch) = &hw.arch {
write!(
&mut res,
"{}: {}",
if arch.len() == 1 {
"Architecture"
} else {
"Architectures"
},
arch.iter().join(", ")
)
.unwrap();
}
if !hw.device.is_empty() {
if !res.is_empty() {
write!(&mut res, "; ").unwrap();
}
write!(
&mut res,
"{}: {}",
if hw.device.len() == 1 {
"Device"
} else {
"Devices"
},
hw.device.iter().map(|d| &d.description).join(", ")
)
.unwrap();
}
if let Some(ram) = hw.ram {
if !res.is_empty() {
write!(&mut res, "; ").unwrap();
}
write!(
&mut res,
"RAM >={:.2}GiB",
ram as f64 / (1024.0 * 1024.0 * 1024.0)
)
.unwrap();
}
res
},
)
.await?;
asset.clone()
}
};
s9pk.validate(SIG_CONTEXT, s9pk.all_signers())?; s9pk.validate(SIG_CONTEXT, s9pk.all_signers())?;
fetching_progress.complete(); fetching_progress.complete();

View File

@@ -1,8 +1,10 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::u32;
use chrono::Utc; use chrono::Utc;
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::ModelExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use url::Url; use url::Url;
@@ -50,7 +52,7 @@ pub struct Category {
pub name: String, pub name: String,
} }
#[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
@@ -62,11 +64,10 @@ pub struct DependencyMetadata {
pub optional: bool, pub optional: bool,
} }
#[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] pub struct PackageMetadata {
pub struct PackageVersionInfo {
#[ts(type = "string")] #[ts(type = "string")]
pub title: InternedString, pub title: InternedString,
pub icon: DataUrl<'static>, pub icon: DataUrl<'static>,
@@ -93,13 +94,11 @@ pub struct PackageVersionInfo {
pub os_version: Version, pub os_version: Version,
#[ts(type = "string | null")] #[ts(type = "string | null")]
pub sdk_version: Option<Version>, pub sdk_version: Option<Version>,
pub hardware_requirements: HardwareRequirements, #[serde(default)]
#[ts(type = "string | null")] pub hardware_acceleration: bool,
pub source_version: Option<VersionRange>,
pub s9pk: RegistryAsset<MerkleArchiveCommitment>,
} }
impl PackageVersionInfo { impl PackageMetadata {
pub async fn from_s9pk<S: FileSource + Clone>(s9pk: &S9pk<S>, url: Url) -> Result<Self, Error> { pub async fn load<S: FileSource + Clone>(s9pk: &S9pk<S>) -> Result<Self, Error> {
let manifest = s9pk.as_manifest(); let manifest = s9pk.as_manifest();
let mut dependency_metadata = BTreeMap::new(); let mut dependency_metadata = BTreeMap::new();
for (id, info) in &manifest.dependencies.0 { for (id, info) in &manifest.dependencies.0 {
@@ -131,67 +130,153 @@ impl PackageVersionInfo {
dependency_metadata, dependency_metadata,
os_version: manifest.os_version.clone(), os_version: manifest.os_version.clone(),
sdk_version: manifest.sdk_version.clone(), sdk_version: manifest.sdk_version.clone(),
hardware_requirements: manifest.hardware_requirements.clone(), hardware_acceleration: manifest.hardware_acceleration.clone(),
source_version: None, // TODO
s9pk: RegistryAsset {
published_at: Utc::now(),
url,
commitment: s9pk.as_archive().commitment().await?,
signatures: [(
AnyVerifyingKey::Ed25519(s9pk.as_archive().signer()),
AnySignature::Ed25519(s9pk.as_archive().signature().await?),
)]
.into_iter()
.collect(),
},
}) })
} }
}
#[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 { pub fn table(&self, version: &VersionString) -> prettytable::Table {
use prettytable::*; use prettytable::*;
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc => &self.title]); table.add_row(row![bc => &self.metadata.title]);
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]); table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]); table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes]);
table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]); table.add_row(
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short, 80).join("\n")],
);
table.add_row(row![ table.add_row(row![
br -> "DESCRIPTION", br -> "DESCRIPTION",
&textwrap::wrap(&self.description.long, 80).join("\n") &textwrap::wrap(&self.metadata.description.long, 80).join("\n")
]); ]);
table.add_row(row![br -> "GIT HASH", self.git_hash.as_deref().unwrap_or("N/A")]); table.add_row(row![br -> "GIT HASH", self.metadata.git_hash.as_deref().unwrap_or("N/A")]);
table.add_row(row![br -> "LICENSE", &self.license]); table.add_row(row![br -> "LICENSE", &self.metadata.license]);
table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); table.add_row(row![br -> "PACKAGE REPO", &self.metadata.wrapper_repo.to_string()]);
table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]); table.add_row(row![br -> "SERVICE REPO", &self.metadata.upstream_repo.to_string()]);
table.add_row(row![br -> "WEBSITE", &self.marketing_site.to_string()]); table.add_row(row![br -> "WEBSITE", &self.metadata.marketing_site.to_string()]);
table.add_row(row![br -> "SUPPORT", &self.support_site.to_string()]); table.add_row(row![br -> "SUPPORT", &self.metadata.support_site.to_string()]);
table table
} }
} }
impl Model<PackageVersionInfo> { impl Model<PackageVersionInfo> {
pub fn works_for_device(&self, device_info: &DeviceInfo) -> Result<bool, Error> { /// Filters this package version for compatibility with the given device.
if !self.as_os_version().de()?.satisfies(&device_info.os.compat) { /// 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); return Ok(false);
} }
let hw = self.as_hardware_requirements().de()?; if let Some(hw) = &device_info.hardware {
if let Some(arch) = hw.arch { self.as_s9pks_mut().mutate(|s9pks| {
if !arch.contains(&device_info.hardware.arch) { s9pks.retain(|(hw_req, _)| {
return Ok(false); if let Some(arch) = &hw_req.arch {
} if !arch.contains(&hw.arch) {
} return false;
if let Some(ram) = hw.ram { }
if device_info.hardware.ram < ram { }
return Ok(false); if let Some(ram) = hw_req.ram {
} if hw.ram < ram {
} return false;
for device_filter in hw.device { }
if !device_info }
.hardware if let Some(dev) = &hw.devices {
.devices for device_filter in &hw_req.device {
.iter() if !dev
.filter(|d| d.class() == &*device_filter.class) .iter()
.any(|d| device_filter.pattern.as_ref().is_match(d.product())) .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); return Ok(false);
} }

View File

@@ -32,14 +32,46 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.no_display() .no_display()
.with_about("Add package to registry index"), .with_about("Add package to registry index"),
) )
.subcommand(
"add-mirror",
from_fn_async(add::add_mirror)
.with_metadata("get_signer", Value::Bool(true))
.no_cli(),
)
.subcommand(
"add-mirror",
from_fn_async(add::cli_add_mirror)
.no_display()
.with_about("Add a mirror for an s9pk"),
)
.subcommand( .subcommand(
"remove", "remove",
from_fn_async(add::remove_package) from_fn_async(add::remove_package)
.with_metadata("get_signer", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_display() .with_custom_display_fn(|args, changed| {
if !changed {
tracing::warn!(
"{}@{}{} does not exist, so not removed",
args.params.id,
args.params.version,
args.params
.sighash
.map_or(String::new(), |h| format!("#{h}"))
);
}
Ok(())
})
.with_about("Remove package from registry index") .with_about("Remove package from registry index")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand(
"remove-mirror",
from_fn_async(add::remove_mirror)
.with_metadata("get_signer", Value::Bool(true))
.no_display()
.with_about("Remove a mirror from a package")
.with_call_remote::<CliContext>(),
)
.subcommand( .subcommand(
"signer", "signer",
signer::signer_api::<C>().with_about("Add, remove, and list package signers"), signer::signer_api::<C>().with_about("Add, remove, and list package signers"),

View File

@@ -7,7 +7,7 @@ use ts_rs::TS;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)]
#[ts(type = "string")] #[ts(type = "string")]
pub struct GitHash(String); pub struct GitHash(String);

View File

@@ -242,18 +242,23 @@ impl TryFrom<ManifestV1> for Manifest {
.device .device
.into_iter() .into_iter()
.map(|(class, product)| DeviceFilter { .map(|(class, product)| DeviceFilter {
pattern_description: format!( description: format!(
"a {class} device matching the expression {}", "a {class} device matching the expression {}",
product.as_ref() product.as_ref()
), ),
class, class,
pattern: product, product: Some(product),
..Default::default()
}) })
.collect(), .collect(),
}, },
git_hash: value.git_hash, git_hash: value.git_hash,
os_version: value.eos_version, os_version: value.eos_version,
sdk_version: None, sdk_version: None,
hardware_acceleration: match value.main {
PackageProcedure::Docker(d) => d.gpu_acceleration,
PackageProcedure::Script(_) => false,
},
}) })
} }
} }

View File

@@ -15,6 +15,7 @@ use crate::s9pk::git_hash::GitHash;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::expected::{Expected, Filter}; use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex; use crate::util::serde::Regex;
use crate::util::{VersionString, mime}; use crate::util::{VersionString, mime};
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
@@ -62,6 +63,8 @@ pub struct Manifest {
pub dependencies: Dependencies, pub dependencies: Dependencies,
#[serde(default)] #[serde(default)]
pub hardware_requirements: HardwareRequirements, pub hardware_requirements: HardwareRequirements,
#[serde(default)]
pub hardware_acceleration: bool,
pub git_hash: Option<GitHash>, pub git_hash: Option<GitHash>,
#[serde(default = "current_version")] #[serde(default = "current_version")]
#[ts(type = "string")] #[ts(type = "string")]
@@ -165,7 +168,7 @@ impl Manifest {
} }
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct HardwareRequirements { pub struct HardwareRequirements {
@@ -176,19 +179,122 @@ pub struct HardwareRequirements {
#[ts(type = "string[] | null")] #[ts(type = "string[] | null")]
pub arch: Option<BTreeSet<InternedString>>, 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, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct DeviceFilter { pub struct DeviceFilter {
pub description: String,
#[ts(type = "\"processor\" | \"display\"")] #[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString, pub class: InternedString,
#[ts(type = "string")] #[ts(type = "string | null")]
pub pattern: Regex, pub product: Option<Regex>,
pub pattern_description: String, #[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)] #[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
#[ts(export)] #[ts(export)]
pub struct Description { pub struct Description {
pub short: String, pub short: String,
@@ -212,7 +318,7 @@ impl Description {
} }
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct Alerts { pub struct Alerts {

View File

@@ -265,7 +265,7 @@ impl PackParams {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct ImageConfig { pub struct ImageConfig {
@@ -274,15 +274,8 @@ pub struct ImageConfig {
pub arch: BTreeSet<InternedString>, pub arch: BTreeSet<InternedString>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
pub emulate_missing_as: Option<InternedString>, pub emulate_missing_as: Option<InternedString>,
} #[serde(default)]
impl Default for ImageConfig { pub nvidia_container: bool,
fn default() -> Self {
Self {
source: ImageSource::Packed,
arch: BTreeSet::new(),
emulate_missing_as: None,
}
}
} }
#[derive(Parser)] #[derive(Parser)]
@@ -299,6 +292,8 @@ struct CliImageConfig {
arch: Vec<InternedString>, arch: Vec<InternedString>,
#[arg(long)] #[arg(long)]
emulate_missing_as: Option<InternedString>, emulate_missing_as: Option<InternedString>,
#[arg(long)]
nvidia_container: bool,
} }
impl TryFrom<CliImageConfig> for ImageConfig { impl TryFrom<CliImageConfig> for ImageConfig {
type Error = clap::Error; type Error = clap::Error;
@@ -317,6 +312,7 @@ impl TryFrom<CliImageConfig> for ImageConfig {
}, },
arch: value.arch.into_iter().collect(), arch: value.arch.into_iter().collect(),
emulate_missing_as: value.emulate_missing_as, emulate_missing_as: value.emulate_missing_as,
nvidia_container: value.nvidia_container,
}; };
res.emulate_missing_as res.emulate_missing_as
.as_ref() .as_ref()
@@ -379,20 +375,21 @@ pub enum ImageSource {
DockerTag(String), DockerTag(String),
// Recipe(DirRecipe), // Recipe(DirRecipe),
} }
impl Default for ImageSource {
fn default() -> Self {
ImageSource::Packed
}
}
impl ImageSource { impl ImageSource {
pub fn ingredients(&self) -> Vec<PathBuf> { pub fn ingredients(&self) -> Vec<PathBuf> {
match self { match self {
Self::Packed => Vec::new(), Self::Packed => Vec::new(),
Self::DockerBuild { Self::DockerBuild { dockerfile, .. } => {
dockerfile,
workdir,
..
} => {
vec![ vec![
workdir dockerfile
.as_deref() .as_deref()
.unwrap_or(Path::new(".")) .unwrap_or(Path::new("Dockerfile"))
.join(dockerfile.as_deref().unwrap_or(Path::new("Dockerfile"))), .to_owned(),
] ]
} }
Self::DockerTag(_) => Vec::new(), Self::DockerTag(_) => Vec::new(),

View File

@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use imbl::OrdMap;
use imbl_value::Value; use imbl_value::Value;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
@@ -53,7 +54,13 @@ impl Context for ContainerCliContext {
} }
impl CallRemote<EffectContext> for ContainerCliContext { impl CallRemote<EffectContext> for ContainerCliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
call_remote_socket( call_remote_socket(
tokio::net::UnixStream::connect(&self.0.socket) tokio::net::UnixStream::connect(&self.0.socket)
.await .await

View File

@@ -6,7 +6,7 @@ use crate::prelude::*;
use crate::service::Service; use crate::service::Service;
#[derive(Clone)] #[derive(Clone)]
pub(in crate::service) struct EffectContext(Weak<Service>); pub struct EffectContext(Weak<Service>);
impl EffectContext { impl EffectContext {
pub fn new(service: Weak<Service>) -> Self { pub fn new(service: Weak<Service>) -> Self {
Self(service) Self(service)

View File

@@ -15,7 +15,7 @@ mod dependency;
mod health; mod health;
mod net; mod net;
mod prelude; mod prelude;
mod subcontainer; pub mod subcontainer;
mod system; mod system;
mod version; mod version;

View File

@@ -11,6 +11,10 @@ use crate::service::effects::prelude::*;
use crate::service::persistent_container::Subcontainer; use crate::service::persistent_container::Subcontainer;
use crate::util::Invoke; use crate::util::Invoke;
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
pub const NVIDIA_OVERLAY_GENERIC: &str = "/var/tmp/startos/nvidia-overlay/generic";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod sync; mod sync;
@@ -112,8 +116,34 @@ pub async fn create_subcontainer_fs(
.with_kind(ErrorKind::Incoherent)?, .with_kind(ErrorKind::Incoherent)?,
); );
tracing::info!("Mounting overlay {guid} for {image_id}"); tracing::info!("Mounting overlay {guid} for {image_id}");
// Determine which nvidia overlay to use based on distro detection
let nvidia_overlay: &[&str] = if context
.seed
.persistent_container
.s9pk
.as_manifest()
.images
.get(&image_id)
.map_or(false, |i| i.nvidia_container)
{
// Check if image is debian-based by looking for /etc/debian_version
let is_debian = tokio::fs::metadata(image.path().join("etc/debian_version"))
.await
.is_ok();
if is_debian && tokio::fs::metadata(NVIDIA_OVERLAY_DEBIAN).await.is_ok() {
&[NVIDIA_OVERLAY_DEBIAN]
} else if tokio::fs::metadata(NVIDIA_OVERLAY_GENERIC).await.is_ok() {
&[NVIDIA_OVERLAY_GENERIC]
} else {
&[]
}
} else {
&[]
};
let subcontainer_wrapper = Subcontainer { let subcontainer_wrapper = Subcontainer {
overlay: OverlayGuard::mount(image, &mountpoint).await?, overlay: OverlayGuard::mount_layers(&[], image, nvidia_overlay, &mountpoint).await?,
name: name name: name
.unwrap_or_else(|| InternedString::intern(format!("subcontainer-{}", image_id))), .unwrap_or_else(|| InternedString::intern(format!("subcontainer-{}", image_id))),
image_id: image_id.clone(), image_id: image_id.clone(),

View File

@@ -9,7 +9,6 @@ use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
use axum::extract::ws::Utf8Bytes; use axum::extract::ws::Utf8Bytes;
use crate::util::net::WebSocket;
use clap::Parser; use clap::Parser;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::stream::FusedStream; use futures::stream::FusedStream;
@@ -48,6 +47,7 @@ use crate::util::Never;
use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::future::NonDetachingJoinHandle; use crate::util::future::NonDetachingJoinHandle;
use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file}; use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
use crate::util::net::WebSocket;
use crate::util::serde::Pem; use crate::util::serde::Pem;
use crate::util::sync::SyncMutex; use crate::util::sync::SyncMutex;
use crate::volume::data_dir; use crate::volume::data_dir;

View File

@@ -96,7 +96,9 @@ impl PersistentContainer {
.join("logs") .join("logs")
.join(&s9pk.as_manifest().id), .join(&s9pk.as_manifest().id),
), ),
LxcConfig::default(), LxcConfig {
hardware_acceleration: s9pk.manifest.hardware_acceleration,
},
) )
.await?; .await?;
let rpc_client = lxc_container.connect_rpc(Some(RPC_CONNECT_TIMEOUT)).await?; let rpc_client = lxc_container.connect_rpc(Some(RPC_CONNECT_TIMEOUT)).await?;

View File

@@ -11,7 +11,7 @@ use crate::sign::commitment::{Commitment, Digestable};
use crate::util::io::TrackingIO; use crate::util::io::TrackingIO;
use crate::util::serde::Base64; use crate::util::serde::Base64;
#[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Clone, Copy, Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]

View File

@@ -51,6 +51,7 @@ impl Model<StatusInfo> {
} }
pub fn stopped(&mut self) -> Result<(), Error> { pub fn stopped(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?; self.as_started_mut().ser(&None)?;
self.as_health_mut().ser(&Default::default())?;
Ok(()) Ok(())
} }
pub fn restart(&mut self) -> Result<(), Error> { pub fn restart(&mut self) -> Result<(), Error> {
@@ -59,7 +60,7 @@ impl Model<StatusInfo> {
Ok(()) Ok(())
} }
pub fn init(&mut self) -> Result<(), Error> { pub fn init(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?; self.stopped()?;
self.as_desired_mut().map_mutate(|s| { self.as_desired_mut().map_mutate(|s| {
Ok(match s { Ok(match s {
DesiredStatus::BackingUp { DesiredStatus::BackingUp {

View File

@@ -251,6 +251,8 @@ impl CallRemote<TunnelContext> for CliContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
_: Empty, _: Empty,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
@@ -315,6 +317,7 @@ impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
TunnelUrlParams { tunnel }: TunnelUrlParams, TunnelUrlParams { tunnel }: TunnelUrlParams,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {

View File

@@ -13,7 +13,7 @@ use ts_rs::TS;
use crate::util::mime::{mime, unmime}; use crate::util::mime::{mime, unmime};
use crate::{Error, ErrorKind, ResultExt}; use crate::{Error, ErrorKind, ResultExt};
#[derive(Clone, TS)] #[derive(Clone, TS, PartialEq, Eq)]
#[ts(type = "string")] #[ts(type = "string")]
pub struct DataUrl<'a> { pub struct DataUrl<'a> {
pub mime: InternedString, pub mime: InternedString,

View File

@@ -16,7 +16,7 @@ use clap::builder::ValueParserFactory;
use futures::future::{BoxFuture, Fuse}; use futures::future::{BoxFuture, Fuse};
use futures::{FutureExt, Stream, TryStreamExt}; use futures::{FutureExt, Stream, TryStreamExt};
use inotify::{EventMask, EventStream, Inotify, WatchMask}; use inotify::{EventMask, EventStream, Inotify, WatchMask};
use nix::unistd::{Gid, Uid}; use nix::unistd::{Gid, Uid, fchown};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::fs::{File, OpenOptions}; use tokio::fs::{File, OpenOptions};
use tokio::io::{ use tokio::io::{
@@ -892,6 +892,16 @@ impl TmpDir {
Ok(()) Ok(())
} }
pub fn leak(mut self) {
std::mem::take(&mut self.path);
}
pub async fn unmount_and_delete(self) -> Result<(), Error> {
crate::disk::mount::util::unmount_all_under(&self.path, false).await?;
tokio::fs::remove_dir_all(&self.path).await?;
Ok(())
}
pub async fn gc(self: Arc<Self>) -> Result<(), Error> { pub async fn gc(self: Arc<Self>) -> Result<(), Error> {
if let Ok(dir) = Arc::try_unwrap(self) { if let Ok(dir) = Arc::try_unwrap(self) {
dir.delete().await dir.delete().await
@@ -1057,6 +1067,32 @@ pub async fn write_file_atomic(
Ok(()) Ok(())
} }
#[instrument(skip_all)]
pub async fn write_file_owned_atomic(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
uid: u32,
gid: u32,
) -> Result<(), Error> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?;
}
let mut file = AtomicFile::new(path, None::<&Path>)
.await
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}")))?;
fchown(&*file, Some(uid.into()), Some(gid.into())).with_kind(ErrorKind::Filesystem)?;
file.write_all(contents.as_ref())
.await
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("write {path:?}")))?;
file.save()
.await
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("save {path:?}")))?;
Ok(())
}
fn poll_flush_prefix<W: AsyncWrite>( fn poll_flush_prefix<W: AsyncWrite>(
mut writer: Pin<&mut W>, mut writer: Pin<&mut W>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,

View File

@@ -1,9 +1,12 @@
use std::collections::BTreeSet;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
use crate::{Error, ResultExt};
const KNOWN_CLASSES: &[&str] = &["processor", "display"]; const KNOWN_CLASSES: &[&str] = &["processor", "display"];
@@ -22,22 +25,57 @@ impl LshwDevice {
Self::Display(_) => "display", Self::Display(_) => "display",
} }
} }
pub fn product(&self) -> &str { pub fn from_value(value: &Value) -> Option<Self> {
match self { match value["class"].as_str() {
Self::Processor(hw) => hw.product.as_str(), Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
Self::Display(hw) => hw.product.as_str(), Some("display") => Some(LshwDevice::Display(LshwDisplay::from_value(value))),
_ => None,
} }
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwProcessor { pub struct LshwProcessor {
pub product: String, pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
}
impl LshwProcessor {
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)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwDisplay { pub struct LshwDisplay {
pub product: String, pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
pub driver: Option<InternedString>,
}
impl LshwDisplay {
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> { pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
@@ -47,19 +85,10 @@ pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
cmd.arg("-class").arg(*class); cmd.arg("-class").arg(*class);
} }
Ok( Ok(
serde_json::from_slice::<Vec<serde_json::Value>>( serde_json::from_slice::<Vec<Value>>(&cmd.invoke(crate::ErrorKind::Lshw).await?)
&cmd.invoke(crate::ErrorKind::Lshw).await?, .with_kind(crate::ErrorKind::Deserialization)?
) .iter()
.with_kind(crate::ErrorKind::Deserialization)? .filter_map(LshwDevice::from_value)
.into_iter() .collect(),
.filter_map(|v| match serde_json::from_value(v) {
Ok(a) => Some(a),
Err(e) => {
tracing::error!("Failed to parse lshw output: {e}");
tracing::debug!("{e:?}");
None
}
})
.collect(),
) )
} }

View File

@@ -1127,6 +1127,11 @@ impl Serialize for Regex {
serialize_display(&self.0, serializer) serialize_display(&self.0, serializer)
} }
} }
impl PartialEq for Regex {
fn eq(&self, other: &Self) -> bool {
InternedString::from_display(self.as_ref()) == InternedString::from_display(other.as_ref())
}
}
// TODO: make this not allocate // TODO: make this not allocate
#[derive(Debug)] #[derive(Debug)]

View File

@@ -95,7 +95,7 @@ pub async fn prompt_multiline<
Ok(res) Ok(res)
} }
pub async fn choose_custom_display<'t, T: std::fmt::Display>( pub async fn choose_custom_display<'t, T>(
prompt: &str, prompt: &str,
choices: &'t [T], choices: &'t [T],
mut display: impl FnMut(&T) -> String, mut display: impl FnMut(&T) -> String,
@@ -121,7 +121,7 @@ pub async fn choose_custom_display<'t, T: std::fmt::Display>(
if choice.len() < 1 { if choice.len() < 1 {
return Err(Error::new(eyre!("Aborted"), ErrorKind::Cancelled)); return Err(Error::new(eyre!("Aborted"), ErrorKind::Cancelled));
} }
let (idx, _) = string_choices let (idx, choice_str) = string_choices
.iter() .iter()
.enumerate() .enumerate()
.find(|(_, s)| s.as_str() == choice[0].as_str()) .find(|(_, s)| s.as_str() == choice[0].as_str())
@@ -132,7 +132,7 @@ pub async fn choose_custom_display<'t, T: std::fmt::Display>(
) )
})?; })?;
let choice = &choices[idx]; let choice = &choices[idx];
println!("{prompt} {choice}"); println!("{prompt} {choice_str}");
Ok(&choice) Ok(&choice)
} }

View File

@@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use tokio::fs::File; use tokio::fs::File;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
@@ -10,7 +11,7 @@ use crate::context::RpcContext;
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::s9pk::manifest::{DeviceFilter, Manifest}; use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::MerkleArchive; use crate::s9pk::merkle_archive::MerkleArchive;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
@@ -84,28 +85,8 @@ impl VersionT for Version {
let mut manifest = previous_manifest.clone(); let mut manifest = previous_manifest.clone();
if let Some(device) = if let Some(_) = previous_manifest["hardwareRequirements"]["device"].as_object() {
previous_manifest["hardwareRequirements"]["device"].as_object() manifest["hardwareRequirements"]["device"] = json!([]);
{
manifest["hardwareRequirements"]["device"] = to_value(
&device
.into_iter()
.map(|(class, product)| {
Ok::<_, Error>(DeviceFilter {
pattern_description: format!(
"a {class} device matching the expression {}",
&product
),
class: class.clone(),
pattern: from_value(product.clone())?,
})
})
.fold(Ok::<_, Error>(Vec::new()), |acc, value| {
let mut acc = acc?;
acc.push(value?);
Ok(acc)
})?,
)?;
} }
if previous_manifest != manifest { if previous_manifest != manifest {

View File

@@ -3,33 +3,39 @@ set -e
SYSTEMCTL=systemctl SYSTEMCTL=systemctl
if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then
SYSTEMCTL=deb-systemd-helper SYSTEMCTL=deb-systemd-helper
fi fi
if [ -f /usr/sbin/grub-probe ] && ! [ -L /usr/sbin/grub-probe ]; then if [ -f /usr/sbin/grub-probe ] && ! [ -L /usr/sbin/grub-probe ]; then
mv /usr/sbin/grub-probe /usr/sbin/grub-probe-default mv /usr/sbin/grub-probe /usr/sbin/grub-probe-default
ln -s /usr/lib/startos/scripts/grub-probe-eos /usr/sbin/grub-probe ln -s /usr/lib/startos/scripts/grub-probe-eos /usr/sbin/grub-probe
fi fi
cp /usr/lib/startos/scripts/startos-initramfs-module /etc/initramfs-tools/scripts/startos cp /usr/lib/startos/scripts/startos-initramfs-module /etc/initramfs-tools/scripts/startos
if ! grep overlay /etc/initramfs-tools/modules > /dev/null; then if ! grep overlay /etc/initramfs-tools/modules > /dev/null; then
echo overlay >> /etc/initramfs-tools/modules echo overlay >> /etc/initramfs-tools/modules
fi fi
update-initramfs -u -k all update-initramfs -u -k all
if [ -f /etc/default/grub ]; then if [ -f /etc/default/grub ]; then
sed -i '/\(^\|#\)GRUB_CMDLINE_LINUX=/c\GRUB_CMDLINE_LINUX="boot=startos console=ttyS0,115200n8"' /etc/default/grub sed -i '/\(^\|#\)GRUB_CMDLINE_LINUX=/c\GRUB_CMDLINE_LINUX="boot=startos console=ttyS0,115200n8 console=tty0"' /etc/default/grub
sed -i '/\(^\|#\)GRUB_DISTRIBUTOR=/c\GRUB_DISTRIBUTOR="StartOS v$(cat /usr/lib/startos/VERSION.txt)"' /etc/default/grub sed -i '/\(^\|#\)GRUB_CMDLINE_LINUX_DEFAULT=/c\GRUB_CMDLINE_LINUX_DEFAULT=""' /etc/default/grub
sed -i '/\(^\|#\)GRUB_TERMINAL=/c\GRUB_TERMINAL="serial"\nGRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' /etc/default/grub sed -i '/\(^\|#\)GRUB_DISTRIBUTOR=/c\GRUB_DISTRIBUTOR="StartOS v$(cat /usr/lib/startos/VERSION.txt)"' /etc/default/grub
sed -i '/\(^\|#\)GRUB_TERMINAL=/c\GRUB_TERMINAL="serial"\nGRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' /etc/default/grub
if grep '^GRUB_SERIAL_COMMAND=' /etc/default/grub > /dev/null; then
sed -i '/\(^\|#\)GRUB_SERIAL_COMMAND=/c\GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' /etc/default/grub
else
echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' >> /etc/default/grub
fi
fi fi
VERSION="$(cat /usr/lib/startos/VERSION.txt)" VERSION="$(cat /usr/lib/startos/VERSION.txt)"
ENVIRONMENT=$(cat /usr/lib/startos/ENVIRONMENT.txt) ENVIRONMENT=$(cat /usr/lib/startos/ENVIRONMENT.txt)
VERSION_ENV="${VERSION}" VERSION_ENV="${VERSION}"
if [ -n "${ENVIRONMENT}" ]; then if [ -n "${ENVIRONMENT}" ]; then
VERSION_ENV="${VERSION} (${ENVIRONMENT})" VERSION_ENV="${VERSION} (${ENVIRONMENT})"
fi fi
# set /etc/os-release # set /etc/os-release
@@ -89,8 +95,8 @@ $SYSTEMCTL mask hibernate.target
$SYSTEMCTL mask hybrid-sleep.target $SYSTEMCTL mask hybrid-sleep.target
if which gsettings > /dev/null; then if which gsettings > /dev/null; then
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout '0' gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout '0'
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout '0' gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout '0'
fi fi
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
@@ -122,7 +128,7 @@ ln -sf /usr/lib/startos/scripts/wireguard-vps-proxy-setup /usr/bin/wireguard-vps
echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-startos.conf echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-startos.conf
if ! getent group | grep '^startos:'; then if ! getent group | grep '^startos:'; then
groupadd startos groupadd startos
fi fi
rm -f /etc/motd rm -f /etc/motd

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddMirrorParams = {
url: string
commitment: MerkleArchiveCommitment
signature: AnySignature
}

View File

@@ -3,7 +3,7 @@ import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddPackageParams = { export type AddPackageParams = {
url: string urls: string[]
commitment: MerkleArchiveCommitment commitment: MerkleArchiveCommitment
signature: AnySignature signature: AnySignature
} }

View File

@@ -1,7 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DeviceFilter = { export type DeviceFilter = {
description: string
class: "processor" | "display" class: "processor" | "display"
pattern: string product: string | null
patternDescription: string vendor: string | null
capabilities?: Array<string>
driver?: string
} }

View File

@@ -7,5 +7,5 @@ export type GetPackageParams = {
id: PackageId | null id: PackageId | null
targetVersion: string | null targetVersion: string | null
sourceVersion: Version | null sourceVersion: Version | null
otherVersions: PackageDetailLevel otherVersions: PackageDetailLevel | null
} }

View File

@@ -5,4 +5,5 @@ export type ImageConfig = {
source: ImageSource source: ImageSource
arch: string[] arch: string[]
emulateMissingAs: string | null emulateMissingAs: string | null
nvidiaContainer: boolean
} }

View File

@@ -1,3 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwDisplay = { product: string } export type LshwDisplay = {
product: string | null
vendor: string | null
capabilities: Array<string>
driver: string | null
}

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwProcessor = { product: string } export type LshwProcessor = {
product: string | null
vendor: string | null
capabilities: Array<string>
}

View File

@@ -31,6 +31,7 @@ export type Manifest = {
alerts: Alerts alerts: Alerts
dependencies: Dependencies dependencies: Dependencies
hardwareRequirements: HardwareRequirements hardwareRequirements: HardwareRequirements
hardwareAcceleration: boolean
gitHash: GitHash | null gitHash: GitHash | null
osVersion: string osVersion: string
sdkVersion: string | null sdkVersion: string | null

View File

@@ -10,6 +10,8 @@ import type { PackageId } from "./PackageId"
import type { RegistryAsset } from "./RegistryAsset" import type { RegistryAsset } from "./RegistryAsset"
export type PackageVersionInfo = { export type PackageVersionInfo = {
sourceVersion: string | null
s9pks: Array<[HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>]>
title: string title: string
icon: DataUrl icon: DataUrl
description: Description description: Description
@@ -26,7 +28,5 @@ export type PackageVersionInfo = {
dependencyMetadata: { [key: PackageId]: DependencyMetadata } dependencyMetadata: { [key: PackageId]: DependencyMetadata }
osVersion: string osVersion: string
sdkVersion: string | null sdkVersion: string | null
hardwareRequirements: HardwareRequirements hardwareAcceleration: boolean
sourceVersion: string | null
s9pk: RegistryAsset<MerkleArchiveCommitment>
} }

View File

@@ -4,7 +4,7 @@ import type { AnyVerifyingKey } from "./AnyVerifyingKey"
export type RegistryAsset<Commitment> = { export type RegistryAsset<Commitment> = {
publishedAt: string publishedAt: string
url: string urls: string[]
commitment: Commitment commitment: Commitment
signatures: { [key: AnyVerifyingKey]: AnySignature } signatures: { [key: AnyVerifyingKey]: AnySignature }
} }

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
export type RemoveMirrorParams = {
id: PackageId
version: Version
url: string
}

View File

@@ -1,5 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Base64 } from "./Base64"
import type { PackageId } from "./PackageId" import type { PackageId } from "./PackageId"
import type { Version } from "./Version" import type { Version } from "./Version"
export type RemovePackageParams = { id: PackageId; version: Version } export type RemovePackageParams = {
id: PackageId
version: Version
sighash: Base64 | null
}

View File

@@ -13,6 +13,7 @@ export { ActionVisibility } from "./ActionVisibility"
export { AddAdminParams } from "./AddAdminParams" export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams" export { AddAssetParams } from "./AddAssetParams"
export { AddCategoryParams } from "./AddCategoryParams" export { AddCategoryParams } from "./AddCategoryParams"
export { AddMirrorParams } from "./AddMirrorParams"
export { AddPackageParams } from "./AddPackageParams" export { AddPackageParams } from "./AddPackageParams"
export { AddPackageSignerParams } from "./AddPackageSignerParams" export { AddPackageSignerParams } from "./AddPackageSignerParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams" export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
@@ -171,6 +172,7 @@ export { RegistryInfo } from "./RegistryInfo"
export { RemoveAdminParams } from "./RemoveAdminParams" export { RemoveAdminParams } from "./RemoveAdminParams"
export { RemoveAssetParams } from "./RemoveAssetParams" export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams" export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemoveMirrorParams } from "./RemoveMirrorParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams" export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams" export { RemovePackageParams } from "./RemovePackageParams"
export { RemovePackageSignerParams } from "./RemovePackageSignerParams" export { RemovePackageSignerParams } from "./RemovePackageSignerParams"

View File

@@ -128,16 +128,17 @@ export type SDKManifest = {
/** /**
* @description (optional) A set of hardware requirements for this service. If the user's machine * @description (optional) A set of hardware requirements for this service. If the user's machine
* does not meet these requirements, they will not be able to install this service. * does not meet these requirements, they will not be able to install this service.
* @property {object[]} devices - TODO Aiden confirm type on the left. List of required devices (displays or processors). * @property {object[]} devices - List of required devices (display or processor).
* `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 {number} ram - Minimum RAM requirement (in megabytes MB)
* @property {string[]} arch - List of supported arches * @property {string[]} arch - List of supported arches
* @example * @example
* ``` * ```
TODO Aiden verify below and provide examples for devices
hardwareRequirements: { hardwareRequirements: {
devices: [ devices: [
{ class: 'display', value: '' }, { class: 'display', pattern: 'CometLake', patternDescription: 'A CometLake (10th generation) Intel Integrated GPU' },
{ class: 'processor', value: '' }, { class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' },
], ],
ram: 8192, ram: 8192,
arch: ['x86-64'], arch: ['x86-64'],
@@ -149,6 +150,11 @@ export type SDKManifest = {
readonly ram?: number | null readonly ram?: number | null
readonly arch?: string[] | null readonly arch?: string[] | null
} }
/**
* @description Enable access to hardware acceleration devices (such as /dev/dri, or /dev/nvidia*)
*/
readonly hardwareAcceleration?: boolean
} }
// this is hacky but idk a more elegant way // this is hacky but idk a more elegant way
@@ -174,6 +180,7 @@ export type SDKImageInputSpec = {
source: Exclude<ImageSource, "packed"> source: Exclude<ImageSource, "packed">
arch?: ArchOptions[A] arch?: ArchOptions[A]
emulateMissingAs?: ArchOptions[A][number] | null emulateMissingAs?: ArchOptions[A][number] | null
nvidiaContainer?: boolean
} }
}[keyof ArchOptions] }[keyof ArchOptions]

View File

@@ -47,6 +47,7 @@ export function buildManifest<
v.emulateMissingAs = (v.arch as string[]).includes("aarch64") v.emulateMissingAs = (v.arch as string[]).includes("aarch64")
? "aarch64" ? "aarch64"
: v.arch[0] || null : v.arch[0] || null
v.nvidiaContainer = !!v.nvidiaContainer
images[k] = v as ImageConfig images[k] = v as ImageConfig
return images return images
}, },
@@ -90,5 +91,6 @@ export function buildManifest<
) )
: manifest.hardwareRequirements?.arch, : manifest.hardwareRequirements?.arch,
}, },
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
} }
} }

View File

@@ -24,7 +24,7 @@ import { MarketplaceItemComponent } from './item.component'
icon="" icon=""
/> />
<!-- release date --> <!-- release date -->
@if (pkg().s9pk?.publishedAt; as published) { @if (pkg().s9pks[0]?.[1]?.publishedAt; as published) {
<marketplace-item <marketplace-item
[style.pointer-events]="'none'" [style.pointer-events]="'none'"
[data]="(published | date: 'medium')!" [data]="(published | date: 'medium')!"

View File

@@ -72,8 +72,8 @@ export class FilterPackagesPipe implements PipeTransform {
.filter(p => category === 'all' || p.categories.includes(category!)) .filter(p => category === 'all' || p.categories.includes(category!))
.sort((a, b) => { .sort((a, b) => {
return ( return (
new Date(b.s9pk.publishedAt).valueOf() - new Date(b.s9pks[0]?.[1].publishedAt!).valueOf() -
new Date(a.s9pk.publishedAt).valueOf() new Date(a.s9pks[0]?.[1].publishedAt!).valueOf()
) )
}) })
.map(a => ({ ...a })) .map(a => ({ ...a }))

View File

@@ -32,10 +32,7 @@ export type StoreData = {
packages: MarketplacePkg[] packages: MarketplacePkg[]
} }
export type MarketplacePkgBase = OptionalProperty< export type MarketplacePkgBase = T.PackageVersionInfo & {
T.PackageVersionInfo,
's9pk'
> & {
id: T.PackageId id: T.PackageId
version: string version: string
flavor: string | null flavor: string | null

View File

@@ -1,10 +1,9 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute } from '@angular/router' import { TuiLoader, TuiNotification } from '@taiga-ui/core'
import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core'
import { NgDompurifyPipe } from '@taiga-ui/dompurify' import { NgDompurifyPipe } from '@taiga-ui/dompurify'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { catchError, ignoreElements, Observable, of } from 'rxjs' import { catchError, ignoreElements, Observable, of, share } from 'rxjs'
import { SafeLinksDirective } from '../directives/safe-links.directive' import { SafeLinksDirective } from '../directives/safe-links.directive'
import { MarkdownPipe } from '../pipes/markdown.pipe' import { MarkdownPipe } from '../pipes/markdown.pipe'
import { getErrorMessage } from '../services/error.service' import { getErrorMessage } from '../services/error.service'
@@ -34,7 +33,10 @@ import { getErrorMessage } from '../services/error.service'
], ],
}) })
export class MarkdownComponent { export class MarkdownComponent {
protected readonly data = injectContext<{ data: Observable<string> }>().data private readonly data = injectContext<{
data: Observable<string>
}>().data.pipe(share())
protected readonly content = toSignal<string>(this.data) protected readonly content = toSignal<string>(this.data)
protected readonly error = toSignal( protected readonly error = toSignal(
this.data.pipe( this.data.pipe(

View File

@@ -5,11 +5,14 @@ import {
InjectionToken, InjectionToken,
input, input,
} from '@angular/core' } from '@angular/core'
import { TuiHintDirective } from '@taiga-ui/core'
import { i18nPipe } from '../i18n/i18n.pipe'
export const VERSION = new InjectionToken<string>('VERSION') export const VERSION = new InjectionToken<string>('VERSION')
@Directive({ @Directive({
selector: '[docsLink]', selector: '[docsLink]',
hostDirectives: [TuiHintDirective],
host: { host: {
target: '_blank', target: '_blank',
rel: 'noreferrer', rel: 'noreferrer',
@@ -20,12 +23,18 @@ export class DocsLinkDirective {
private readonly version = inject(VERSION) private readonly version = inject(VERSION)
readonly path = input.required<string>() readonly path = input.required<string>()
readonly fragment = input<string>('') readonly fragment = input<string>('')
protected readonly url = computed(() => { protected readonly url = computed(() => {
const path = this.path() const path = this.path()
const relative = path.startsWith('/') ? path : `/${path}` const relative = path.startsWith('/') ? path : `/${path}`
return `https://docs.start9.com${relative}?os=${this.version}${this.fragment()}` return `https://docs.start9.com${relative}?os=${this.version}${this.fragment()}`
}) })
constructor() {
inject(TuiHintDirective).content.set(
inject(i18nPipe).transform('Documentation'),
)
}
} }

View File

@@ -596,4 +596,5 @@ export default {
626: 'Hochladen', 626: 'Hochladen',
627: 'UI öffnen', 627: 'UI öffnen',
628: 'In Zwischenablage kopiert', 628: 'In Zwischenablage kopiert',
629: 'Die Liste ist leer',
} satisfies i18n } satisfies i18n

View File

@@ -595,4 +595,5 @@ export const ENGLISH = {
'Upload': 626, // as in, upload a file 'Upload': 626, // as in, upload a file
'Open UI': 627, // as in, upload a file 'Open UI': 627, // as in, upload a file
'Copied to clipboard': 628, 'Copied to clipboard': 628,
} as const 'The list is empty': 629,
} as Record<any, any>

View File

@@ -596,4 +596,5 @@ export default {
626: 'Subir', 626: 'Subir',
627: 'Abrir UI', 627: 'Abrir UI',
628: 'Copiado al portapapeles', 628: 'Copiado al portapapeles',
629: 'La lista está vacía',
} satisfies i18n } satisfies i18n

View File

@@ -596,4 +596,5 @@ export default {
626: 'Téléverser', 626: 'Téléverser',
627: 'Ouvrir UI', 627: 'Ouvrir UI',
628: 'Copié dans le presse-papiers', 628: 'Copié dans le presse-papiers',
629: 'La liste est vide',
} satisfies i18n } satisfies i18n

View File

@@ -596,4 +596,5 @@ export default {
626: 'Prześlij', 626: 'Prześlij',
627: 'Otwórz UI', 627: 'Otwórz UI',
628: 'Skopiowano do schowka', 628: 'Skopiowano do schowka',
629: 'Lista jest pusta',
} satisfies i18n } satisfies i18n

View File

@@ -20,6 +20,7 @@ import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
import { import {
TUI_DATE_FORMAT, TUI_DATE_FORMAT,
TUI_DIALOGS_CLOSE, TUI_DIALOGS_CLOSE,
TUI_MEDIA,
tuiAlertOptionsProvider, tuiAlertOptionsProvider,
tuiButtonOptionsProvider, tuiButtonOptionsProvider,
tuiDropdownOptionsProvider, tuiDropdownOptionsProvider,
@@ -140,4 +141,12 @@ export const APP_PROVIDERS = [
none: identity, none: identity,
}, },
}), }),
{
provide: TUI_MEDIA,
useValue: {
mobile: 1000,
desktopSmall: 1280,
desktopLarge: Infinity,
},
},
] ]

View File

@@ -80,7 +80,7 @@ export interface FormContext<T> {
margin: 1rem -1px -1rem; margin: 1rem -1px -1rem;
gap: 1rem; gap: 1rem;
background: var(--tui-background-elevation-1); background: var(--tui-background-elevation-1);
border-top: 1px solid var(--tui-background-base-alt); box-shadow: inset 0 1px var(--tui-background-neutral-1);
} }
`, `,
imports: [ imports: [

View File

@@ -87,6 +87,8 @@ import { FormObjectComponent } from './object.component'
} }
</div> </div>
</div> </div>
} @empty {
<div class="placeholder">{{ 'The list is empty' | i18n }}</div>
} }
`, `,
styles: ` styles: `
@@ -99,8 +101,8 @@ import { FormObjectComponent } from './object.component'
.label { .label {
display: flex; display: flex;
font-size: 1.25rem; align-items: center;
font-weight: bold; font: var(--tui-font-heading-6);
} }
.add { .add {
@@ -157,6 +159,17 @@ import { FormObjectComponent } from './object.component'
animation-name: tuiFade, tuiCollapse; animation-name: tuiFade, tuiCollapse;
} }
} }
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: var(--tui-height-m);
color: var(--tui-text-tertiary);
border-radius: var(--tui-radius-m);
border: 1px dashed var(--tui-background-neutral-1);
margin: 0.5rem 0 0;
}
`, `,
hostDirectives: [ControlDirective], hostDirectives: [ControlDirective],
imports: [ imports: [

View File

@@ -1,12 +1,11 @@
import { Component } from '@angular/core' import { Component, computed } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { invert } from '@start9labs/shared' import { invert } from '@start9labs/shared'
import { IST } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiIcon, TuiTextfield } from '@taiga-ui/core' import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
import { TuiChevron, TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit' import { TuiChevron, TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
import { Control } from './control'
import { HintPipe } from '../pipes/hint.pipe' import { HintPipe } from '../pipes/hint.pipe'
import { Control } from './control'
@Component({ @Component({
selector: 'form-multiselect', selector: 'form-multiselect',
@@ -21,7 +20,8 @@ import { HintPipe } from '../pipes/hint.pipe'
[disabled]="disabled" [disabled]="disabled"
[readOnly]="readOnly" [readOnly]="readOnly"
[items]="items" [items]="items"
[(ngModel)]="selected" [ngModel]="selected()"
(ngModelChange)="onSelected($event)"
(blur)="control.onTouched()" (blur)="control.onTouched()"
></select> ></select>
@if (spec | hint; as hint) { @if (spec | hint; as hint) {
@@ -58,30 +58,26 @@ export class FormMultiselectComponent extends Control<
private readonly isExceedingLimit = (item: string) => private readonly isExceedingLimit = (item: string) =>
!!this.spec.maxLength && !!this.spec.maxLength &&
this.selected.length >= this.spec.maxLength && this.selected().length >= this.spec.maxLength &&
!this.selected.includes(item) !this.selected().includes(item)
readonly disabledItemHandler = (item: string): boolean => readonly disabledItemHandler = (item: string): boolean =>
this.isDisabled(item) || this.isExceedingLimit(item) this.isDisabled(item) || this.isExceedingLimit(item)
readonly items = Object.values(this.spec.values) readonly items = Object.values(this.spec.values)
readonly selected = computed(
() =>
this.control.value().map((key: string) => this.spec.values[key] || '') ||
[],
)
get disabled(): boolean { get disabled(): boolean {
return typeof this.spec.disabled === 'string' return typeof this.spec.disabled === 'string'
} }
get selected(): string[] { onSelected(value: string[]) {
return this.memoize(this.value)
}
set selected(value: string[]) {
this.value = Object.entries(this.spec.values) this.value = Object.entries(this.spec.values)
.filter(([_, v]) => value.includes(v)) .filter(([_, v]) => value.includes(v))
.map(([k]) => k) .map(([k]) => k)
} }
@tuiPure
private memoize(value: null | readonly string[]): string[] {
return value?.map(key => this.spec.values[key] || '') || []
}
} }

View File

@@ -111,11 +111,11 @@ import { ConfigService } from 'src/app/services/config.service'
&-list { &-list {
display: grid; display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 4rem 3rem; gap: 3.5rem 2.5rem;
padding: 1.5rem; padding: 1.5rem;
@media (min-width: 768px) { @media (min-width: 768px) {
padding: 2rem; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -10,7 +10,7 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { from, map } from 'rxjs' import { defer, map } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
@@ -54,14 +54,13 @@ import {
}) })
export default class ServiceAboutRoute { export default class ServiceAboutRoute {
private readonly pkgId = getPkgId() private readonly pkgId = getPkgId()
private readonly api = inject(ApiService)
private readonly copyService = inject(CopyService) private readonly copyService = inject(CopyService)
private readonly markdown = inject(DialogService).openComponent(MARKDOWN, { private readonly markdown = inject(DialogService).openComponent(MARKDOWN, {
label: 'License', label: 'License',
size: 'l', size: 'l',
data: from( data: defer(() =>
inject(ApiService).getStatic( this.api.getStatic([`/s9pk/installed/${this.pkgId}.s9pk/LICENSE.md`], {}),
`/s9pk/installed/${this.pkgId}.s9pk/LICENSE.md`,
),
), ),
}) })

View File

@@ -41,6 +41,7 @@ async function parseS9pk(file: File): Promise<MarketplacePkgSideload> {
sourceVersion: s9pk.manifest.canMigrateFrom, sourceVersion: s9pk.manifest.canMigrateFrom,
flavor: ExtendedVersion.parse(s9pk.manifest.version).flavor, flavor: ExtendedVersion.parse(s9pk.manifest.version).flavor,
fullLicense: await s9pk.license(), fullLicense: await s9pk.license(),
s9pks: [],
} }
} }

View File

@@ -70,7 +70,7 @@ import UpdatesComponent from './updates.component'
</div> </div>
</td> </td>
<td class="desktop">{{ item().gitHash }}</td> <td class="desktop">{{ item().gitHash }}</td>
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td> <td class="desktop">{{ item().s9pks[0]?.[1]?.publishedAt | date }}</td>
<td> <td>
<button <button
tuiIconButton tuiIconButton
@@ -114,7 +114,9 @@ import UpdatesComponent from './updates.component'
</p> </p>
<p tuiTitle class="mobile"> <p tuiTitle class="mobile">
<b>{{ 'Published' | i18n }}</b> <b>{{ 'Published' | i18n }}</b>
<span tuiSubtitle>{{ item().s9pk.publishedAt | date }}</span> <span tuiSubtitle>
{{ item().s9pks[0]?.[1]?.publishedAt | date }}
</span>
</p> </p>
<p tuiTitle> <p tuiTitle>
<span> <span>

View File

@@ -40,7 +40,9 @@ export namespace Mock {
squashfs: { squashfs: {
aarch64: { aarch64: {
publishedAt: '2025-03-21T23:55:29.583006392Z', publishedAt: '2025-03-21T23:55:29.583006392Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64.squashfs',
],
commitment: { commitment: {
hash: 'OUnANnZePtf7rSbj38JESl+iJAV0z0aiZ4opCiwpGbo=', hash: 'OUnANnZePtf7rSbj38JESl+iJAV0z0aiZ4opCiwpGbo=',
size: 1331900416, size: 1331900416,
@@ -52,7 +54,9 @@ export namespace Mock {
}, },
'aarch64-nonfree': { 'aarch64-nonfree': {
publishedAt: '2025-03-21T23:56:38.299572946Z', publishedAt: '2025-03-21T23:56:38.299572946Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64-nonfree.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_aarch64-nonfree.squashfs',
],
commitment: { commitment: {
hash: '6k+0RcyRQV+5A+h06OqpHxd4IT6IlFkfdy9dfHIP90c=', hash: '6k+0RcyRQV+5A+h06OqpHxd4IT6IlFkfdy9dfHIP90c=',
size: 1641500672, size: 1641500672,
@@ -64,7 +68,9 @@ export namespace Mock {
}, },
raspberrypi: { raspberrypi: {
publishedAt: '2025-03-22T00:08:17.083064390Z', publishedAt: '2025-03-22T00:08:17.083064390Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_raspberrypi.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_raspberrypi.squashfs',
],
commitment: { commitment: {
hash: 'K+XuTZxo1KVsKjNSV8PPOMruCvAEZwerF9mbpFl53Gk=', hash: 'K+XuTZxo1KVsKjNSV8PPOMruCvAEZwerF9mbpFl53Gk=',
size: 1544417280, size: 1544417280,
@@ -76,7 +82,9 @@ export namespace Mock {
}, },
x86_64: { x86_64: {
publishedAt: '2025-03-22T00:05:57.684319247Z', publishedAt: '2025-03-22T00:05:57.684319247Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64.squashfs',
],
commitment: { commitment: {
hash: '3UVkx3TQMBPlSU1OnV48Om9vjjA3s+Nk6dX3auYGpBo=', hash: '3UVkx3TQMBPlSU1OnV48Om9vjjA3s+Nk6dX3auYGpBo=',
size: 1424007168, size: 1424007168,
@@ -88,7 +96,9 @@ export namespace Mock {
}, },
'x86_64-nonfree': { 'x86_64-nonfree': {
publishedAt: '2025-03-22T00:07:11.893777122Z', publishedAt: '2025-03-22T00:07:11.893777122Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64-nonfree.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.3.6-alpha.17/startos-0.3.6-alpha.17-b8ff331~dev_x86_64-nonfree.squashfs',
],
commitment: { commitment: {
hash: 'IS1gJ56n/HlQqFbl1upMOAtLxyxB0cY0H89Ha+9h1lE=', hash: 'IS1gJ56n/HlQqFbl1upMOAtLxyxB0cY0H89Ha+9h1lE=',
size: 1743425536, size: 1743425536,
@@ -110,7 +120,9 @@ export namespace Mock {
squashfs: { squashfs: {
aarch64: { aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z', publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64.squashfs',
],
commitment: { commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288, size: 1343500288,
@@ -122,7 +134,9 @@ export namespace Mock {
}, },
'aarch64-nonfree': { 'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z', publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64-nonfree.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64-nonfree.squashfs',
],
commitment: { commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968, size: 1653075968,
@@ -134,7 +148,9 @@ export namespace Mock {
}, },
raspberrypi: { raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z', publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_raspberrypi.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_raspberrypi.squashfs',
],
commitment: { commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008, size: 1490731008,
@@ -146,7 +162,9 @@ export namespace Mock {
}, },
x86_64: { x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z', publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64.squashfs',
],
commitment: { commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728, size: 1411657728,
@@ -158,7 +176,9 @@ export namespace Mock {
}, },
'x86_64-nonfree': { 'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z', publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64-nonfree.squashfs', urls: [
'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64-nonfree.squashfs',
],
commitment: { commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136, size: 1731035136,
@@ -234,6 +254,7 @@ export namespace Mock {
source: 'packed', source: 'packed',
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
emulateMissingAs: 'aarch64', emulateMissingAs: 'aarch64',
nvidiaContainer: false,
}, },
}, },
volumes: ['main'], volumes: ['main'],
@@ -242,6 +263,7 @@ export namespace Mock {
arch: null, arch: null,
ram: null, ram: null,
}, },
hardwareAcceleration: false,
} }
export const MockManifestLnd: T.Manifest = { export const MockManifestLnd: T.Manifest = {
@@ -292,6 +314,7 @@ export namespace Mock {
source: 'packed', source: 'packed',
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
emulateMissingAs: 'aarch64', emulateMissingAs: 'aarch64',
nvidiaContainer: false,
}, },
}, },
volumes: ['main'], volumes: ['main'],
@@ -300,6 +323,7 @@ export namespace Mock {
arch: null, arch: null,
ram: null, ram: null,
}, },
hardwareAcceleration: false,
} }
export const MockManifestBitcoinProxy: T.Manifest = { export const MockManifestBitcoinProxy: T.Manifest = {
@@ -343,6 +367,7 @@ export namespace Mock {
source: 'packed', source: 'packed',
arch: ['x86_64', 'aarch64'], arch: ['x86_64', 'aarch64'],
emulateMissingAs: 'aarch64', emulateMissingAs: 'aarch64',
nvidiaContainer: false,
}, },
}, },
volumes: ['main'], volumes: ['main'],
@@ -351,6 +376,7 @@ export namespace Mock {
arch: null, arch: null,
ram: null, ram: null,
}, },
hardwareAcceleration: false,
} }
export const BitcoinDep: T.DependencyMetadata = { export const BitcoinDep: T.DependencyMetadata = {
@@ -376,7 +402,6 @@ export namespace Mock {
'26.1.0:0.1.0': { '26.1.0:0.1.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -398,12 +423,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
'#knots:26.1.20240325:0': { '#knots:26.1.20240325:0': {
title: 'Bitcoin Knots', title: 'Bitcoin Knots',
@@ -411,7 +444,6 @@ export namespace Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -433,12 +465,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['bitcoin', 'featured'], categories: ['bitcoin', 'featured'],
@@ -456,7 +496,6 @@ export namespace Mock {
'26.1.0:0.1.0': { '26.1.0:0.1.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -478,12 +517,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
'#knots:26.1.20240325:0': { '#knots:26.1.20240325:0': {
title: 'Bitcoin Knots', title: 'Bitcoin Knots',
@@ -491,7 +538,6 @@ export namespace Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -513,12 +559,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['bitcoin', 'featured'], categories: ['bitcoin', 'featured'],
@@ -538,7 +592,6 @@ export namespace Mock {
'0.17.5:0': { '0.17.5:0': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -563,12 +616,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['lightning'], categories: ['lightning'],
@@ -586,7 +647,6 @@ export namespace Mock {
'0.17.4-beta:1.0-alpha': { '0.17.4-beta:1.0-alpha': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -611,12 +671,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['lightning'], categories: ['lightning'],
@@ -638,7 +706,6 @@ export namespace Mock {
'27.0.0:1.0.0': { '27.0.0:1.0.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -660,12 +727,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
'#knots:27.1.0:0': { '#knots:27.1.0:0': {
title: 'Bitcoin Knots', title: 'Bitcoin Knots',
@@ -673,7 +748,6 @@ export namespace Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -695,12 +769,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['bitcoin', 'featured'], categories: ['bitcoin', 'featured'],
@@ -718,7 +800,6 @@ export namespace Mock {
'0.18.0:0.0.1': { '0.18.0:0.0.1': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -743,12 +824,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['lightning'], categories: ['lightning'],
@@ -766,7 +855,6 @@ export namespace Mock {
'0.3.2.7:0': { '0.3.2.7:0': {
title: 'Bitcoin Proxy', title: 'Bitcoin Proxy',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -790,12 +878,20 @@ export namespace Mock {
stop: 'test', stop: 'test',
restore: 'test', restore: 'test',
}, },
s9pk: { s9pks: [
url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk', [
commitment: mockMerkleArchiveCommitment, { arch: null, device: [], ram: null },
signatures: {}, {
publishedAt: Date.now().toString(), urls: [
}, 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk',
],
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
],
],
hardwareAcceleration: false,
}, },
}, },
categories: ['bitcoin'], categories: ['bitcoin'],

View File

@@ -10,13 +10,11 @@ export abstract class ApiService {
abstract uploadFile(guid: string, body: Blob): Promise<void> abstract uploadFile(guid: string, body: Blob): Promise<void>
// for getting static files: ex license // for getting static files: ex license
abstract getStaticProxy( abstract getStatic(
pkg: MarketplacePkg, urls: string[],
path: 'LICENSE.md', params: Record<string, string | number>,
): Promise<string> ): Promise<string>
abstract getStatic(url: string): Promise<string>
// websocket // websocket
abstract openWebsocket$<T>( abstract openWebsocket$<T>(

View File

@@ -45,29 +45,22 @@ export class LiveApiService extends ApiService {
// for getting static files: ex: license // for getting static files: ex: license
async getStaticProxy( async getStatic(
pkg: MarketplacePkg, urls: string[],
path: 'LICENSE.md', params: Record<string, string | number>,
): Promise<string> { ): Promise<string> {
const encodedUrl = encodeURIComponent(pkg.s9pk.url) for (let url in urls) {
try {
return this.httpRequest({ const res = await this.httpRequest<string>({
method: 'GET', method: 'GET',
url: `/s9pk/proxy/${encodedUrl}/${path}`, url,
params: { params,
rootSighash: pkg.s9pk.commitment.rootSighash, responseType: 'text',
rootMaxsize: pkg.s9pk.commitment.rootMaxsize, })
}, return res
responseType: 'text', } catch (e) {}
}) }
} throw new Error('Could not fetch static file')
async getStatic(url: string): Promise<string> {
return this.httpRequest({
method: 'GET',
url,
responseType: 'text',
})
} }
// websocket // websocket

View File

@@ -75,19 +75,14 @@ export class MockApiService extends ApiService {
await pauseFor(2000) await pauseFor(2000)
} }
async getStaticProxy( async getStatic(
pkg: MarketplacePkg, urls: string[],
path: 'LICENSE.md', params: Record<string, string | number>,
): Promise<string> { ): Promise<string> {
await pauseFor(2000) await pauseFor(2000)
return markdown return markdown
} }
async getStatic(url: string): Promise<string> {
await pauseFor(2000)
return markdown
}
// websocket // websocket
openWebsocket$<T>( openWebsocket$<T>(

View File

@@ -142,7 +142,23 @@ export class MarketplaceService {
} }
fetchStatic$(pkg: MarketplacePkg): Observable<string> { fetchStatic$(pkg: MarketplacePkg): Observable<string> {
return from(this.api.getStaticProxy(pkg, 'LICENSE.md')) const registryAsset = pkg.s9pks[0]?.[1]
if (!registryAsset) {
throw new Error('No s9pk')
}
const urls =
registryAsset.urls.map(
u => `/s9pk/proxy/${encodeURIComponent(u)}/LICENSE.md`,
) || []
return from(
this.api.getStatic(urls, {
rootSighash: registryAsset.commitment.rootSighash,
rootMaxsize: registryAsset.commitment.rootMaxsize,
}),
)
} }
private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> { private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {