Feature/fe new registry (#2647)

* bugfixes

* update fe types

* implement new registry types in marketplace and ui

* fix marketplace types to have default params

* add alt implementation toggle

* merge cleanup

* more cleanup and notes

* fix build

* cleanup sync with next/minor

* add exver JS parser

* parse ValidExVer to string

* update types to interface

* add VersionRange and comparative functions

* Parse ExtendedVersion from string

* add conjunction, disjunction, and inversion logic

* consider flavor in satisfiedBy fn

* consider prerelease for ordering

* add compare fn for sorting

* rename fns for consistency

* refactoring

* update compare fn to return null if flavors don't match

* begin simplifying dependencies

* under construction

* wip

* add dependency metadata to CurrentDependencyInfo

* ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip

* preprocess manifest

* misc fixes

* use sdk version as osVersion in manifest

* chore: Change the type to just validate and not generate all solutions.

* add publishedAt

* fix pegjs exports

* integrate exver into sdk

* misc fixes

* complete satisfiedBy fn

* refactor - use greaterThanOrEqual and lessThanOrEqual fns

* fix tests

* update dependency details

* update types

* remove interim types

* rename alt implementation to flavor

* cleanup os update

* format exver.ts

* add s9pk parsing endpoints

* fix build

* update to exver

* exver and bug fixes

* update static endpoints + cleanup

* cleanup

* update static proxy verification

* make mocks more robust; fix dep icon fallback; cleanup

* refactor alert versions and update fixtures

* registry bugfixes

* misc fixes

* cleanup unused

* convert patchdb ui seed to camelCase

* update otherVersions type

* change otherVersions: null to 'none'

* refactor and complete feature

* improve static endpoints

* fix install params

* mask systemd-networkd-wait-online

* fix static file fetching

* include non-matching versions in otherVersions

* convert release notes to modal and clean up displayExver

* alert for no other versions

* Fix ack-instructions casing

* fix indeterminate loader on service install

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Lucy
2024-07-22 20:48:12 -04:00
committed by GitHub
parent 0fbb18b315
commit a535fc17c3
196 changed files with 7002 additions and 2162 deletions

View File

@@ -1,4 +1,4 @@
import { types as T, utils, EmVer } from "@start9labs/start-sdk" import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk"
import * as fs from "fs/promises" import * as fs from "fs/promises"
import { polyfillEffects } from "./polyfillEffects" import { polyfillEffects } from "./polyfillEffects"
@@ -647,13 +647,13 @@ export class SystemForEmbassy implements System {
dependencies: Object.entries(dependsOn).flatMap(([key, value]) => { dependencies: Object.entries(dependsOn).flatMap(([key, value]) => {
const dependency = this.manifest.dependencies?.[key] const dependency = this.manifest.dependencies?.[key]
if (!dependency) return [] if (!dependency) return []
const versionSpec = dependency.version const versionRange = dependency.version
const registryUrl = DEFAULT_REGISTRY const registryUrl = DEFAULT_REGISTRY
const kind = "running" const kind = "running"
return [ return [
{ {
id: key, id: key,
versionSpec, versionRange,
registryUrl, registryUrl,
kind, kind,
healthChecks: [...value], healthChecks: [...value],
@@ -668,18 +668,24 @@ export class SystemForEmbassy implements System {
fromVersion: string, fromVersion: string,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<T.MigrationRes> { ): Promise<T.MigrationRes> {
const fromEmver = EmVer.from(fromVersion) const fromEmver = ExtendedVersion.parseEmver(fromVersion)
const currentEmver = EmVer.from(this.manifest.version) const currentEmver = ExtendedVersion.parseEmver(this.manifest.version)
if (!this.manifest.migrations) return { configured: true } if (!this.manifest.migrations) return { configured: true }
const fromMigration = Object.entries(this.manifest.migrations.from) const fromMigration = Object.entries(this.manifest.migrations.from)
.map(([version, procedure]) => [EmVer.from(version), procedure] as const) .map(
([version, procedure]) =>
[ExtendedVersion.parseEmver(version), procedure] as const,
)
.find( .find(
([versionEmver, procedure]) => ([versionEmver, procedure]) =>
versionEmver.greaterThan(fromEmver) && versionEmver.greaterThan(fromEmver) &&
versionEmver.lessThanOrEqual(currentEmver), versionEmver.lessThanOrEqual(currentEmver),
) )
const toMigration = Object.entries(this.manifest.migrations.to) const toMigration = Object.entries(this.manifest.migrations.to)
.map(([version, procedure]) => [EmVer.from(version), procedure] as const) .map(
([version, procedure]) =>
[ExtendedVersion.parseEmver(version), procedure] as const,
)
.find( .find(
([versionEmver, procedure]) => ([versionEmver, procedure]) =>
versionEmver.greaterThan(fromEmver) && versionEmver.greaterThan(fromEmver) &&

51
core/Cargo.lock generated
View File

@@ -471,9 +471,9 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]] [[package]]
name = "base32" name = "base32"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
[[package]] [[package]]
name = "base64" name = "base64"
@@ -4632,6 +4632,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.7" version = "0.5.7"
@@ -4953,7 +4959,7 @@ dependencies = [
"axum-server", "axum-server",
"backhand", "backhand",
"barrage", "barrage",
"base32 0.5.0", "base32 0.5.1",
"base64 0.22.1", "base64 0.22.1",
"base64ct", "base64ct",
"basic-cookies", "basic-cookies",
@@ -4975,6 +4981,7 @@ dependencies = [
"ed25519-dalek 2.1.1", "ed25519-dalek 2.1.1",
"exver", "exver",
"fd-lock-rs", "fd-lock-rs",
"form_urlencoded",
"futures", "futures",
"gpt", "gpt",
"helpers", "helpers",
@@ -5044,6 +5051,7 @@ dependencies = [
"sscanf", "sscanf",
"ssh-key", "ssh-key",
"tar", "tar",
"textwrap",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@@ -5052,7 +5060,7 @@ dependencies = [
"tokio-tar", "tokio-tar",
"tokio-tungstenite 0.23.1", "tokio-tungstenite 0.23.1",
"tokio-util", "tokio-util",
"toml 0.8.14", "toml 0.8.15",
"torut", "torut",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -5221,6 +5229,17 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]] [[package]]
name = "thingbuf" name = "thingbuf"
version = "0.1.6" version = "0.1.6"
@@ -5233,18 +5252,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.62" version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.62" version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5480,14 +5499,14 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.14" version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.15", "toml_edit 0.22.16",
] ]
[[package]] [[package]]
@@ -5525,9 +5544,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.15" version = "0.22.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",
@@ -5888,6 +5907,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.23" version = "0.1.23"

View File

@@ -90,6 +90,7 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git",
"serde", "serde",
] } ] }
fd-lock-rs = "0.1.4" fd-lock-rs = "0.1.4"
form_urlencoded = "1.2.1"
futures = "0.3.28" futures = "0.3.28"
gpt = "3.1.0" gpt = "3.1.0"
helpers = { path = "../helpers" } helpers = { path = "../helpers" }
@@ -171,6 +172,7 @@ sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] } ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40" tar = "0.4.40"
thiserror = "1.0.49" thiserror = "1.0.49"
textwrap = "0.16.1"
tokio = { version = "1.38.1", features = ["full"] } tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.0" tokio-rustls = "0.26.0"
tokio-socks = "0.5.1" tokio-socks = "0.5.1"

View File

@@ -8,6 +8,7 @@ use color_eyre::eyre::eyre;
use digest::generic_array::GenericArray; use digest::generic_array::GenericArray;
use digest::OutputSizeUser; use digest::OutputSizeUser;
use exver::Version; use exver::Version;
use imbl_value::InternedString;
use models::PackageId; use models::PackageId;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -213,7 +214,7 @@ pub struct BackupInfo {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PackageBackupInfo { pub struct PackageBackupInfo {
pub title: String, pub title: InternedString,
pub version: VersionString, pub version: VersionString,
pub os_version: Version, pub os_version: Version,
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,

View File

@@ -141,6 +141,7 @@ async fn setup_or_init(
} else { } else {
let init_ctx = InitContext::init(config).await?; let init_ctx = InitContext::init(config).await?;
let handle = init_ctx.progress.clone(); let handle = init_ctx.progress.clone();
let err_channel = init_ctx.error.clone();
let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10));
let init_phases = InitPhases::new(&handle); let init_phases = InitPhases::new(&handle);
@@ -148,47 +149,55 @@ async fn setup_or_init(
server.serve_init(init_ctx); server.serve_init(init_ctx);
disk_phase.start(); async {
let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy disk_phase.start();
let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?;
let disk_guid = Arc::new(String::from(guid_string.trim()));
let requires_reboot = crate::disk::main::import(
&**disk_guid,
config.datadir(),
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive
} else {
RepairStrategy::Preen
},
if disk_guid.ends_with("_UNENC") {
None
} else {
Some(DEFAULT_PASSWORD)
},
)
.await?; .await?;
let disk_guid = Arc::new(String::from(guid_string.trim()));
let requires_reboot = crate::disk::main::import(
&**disk_guid,
config.datadir(),
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive tokio::fs::remove_file(REPAIR_DISK_PATH)
} else { .await
RepairStrategy::Preen .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}, }
if disk_guid.ends_with("_UNENC") { disk_phase.complete();
None tracing::info!("Loaded Disk");
} else {
Some(DEFAULT_PASSWORD) if requires_reboot.0 {
}, let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
) reboot_phase.start();
.await?; return Ok(Err(Shutdown {
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { export_args: Some((disk_guid, config.datadir().to_owned())),
tokio::fs::remove_file(REPAIR_DISK_PATH) restart: true,
.await }));
.with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; }
let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?;
let rpc_ctx =
RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?;
Ok::<_, Error>(Ok((rpc_ctx, handle)))
} }
disk_phase.complete(); .await
tracing::info!("Loaded Disk"); .map_err(|e| {
err_channel.send_replace(Some(e.clone_output()));
if requires_reboot.0 { e
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); })
reboot_phase.start();
return Ok(Err(Shutdown {
export_args: Some((disk_guid, config.datadir().to_owned())),
restart: true,
}));
}
let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?;
let rpc_ctx = RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?;
Ok(Ok((rpc_ctx, handle)))
} }
} }

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use rpc_toolkit::Context; use rpc_toolkit::Context;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tokio::sync::watch;
use tracing::instrument; use tracing::instrument;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
@@ -12,6 +13,7 @@ use crate::Error;
pub struct InitContextSeed { pub struct InitContextSeed {
pub config: ServerConfig, pub config: ServerConfig,
pub error: watch::Sender<Option<Error>>,
pub progress: FullProgressTracker, pub progress: FullProgressTracker,
pub shutdown: Sender<()>, pub shutdown: Sender<()>,
pub rpc_continuations: RpcContinuations, pub rpc_continuations: RpcContinuations,
@@ -25,6 +27,7 @@ impl InitContext {
let (shutdown, _) = tokio::sync::broadcast::channel(1); let (shutdown, _) = tokio::sync::broadcast::channel(1);
Ok(Self(Arc::new(InitContextSeed { Ok(Self(Arc::new(InitContextSeed {
config: cfg.clone(), config: cfg.clone(),
error: watch::channel(None).0,
progress: FullProgressTracker::new(), progress: FullProgressTracker::new(),
shutdown, shutdown,
rpc_continuations: RpcContinuations::new(), rpc_continuations: RpcContinuations::new(),

View File

@@ -372,14 +372,13 @@ impl Map for CurrentDependencies {
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CurrentDependencyInfo { pub struct CurrentDependencyInfo {
#[ts(type = "string | null")]
pub title: Option<InternedString>,
pub icon: Option<DataUrl<'static>>,
#[serde(flatten)] #[serde(flatten)]
pub kind: CurrentDependencyKind, pub kind: CurrentDependencyKind,
pub title: String,
pub icon: DataUrl<'static>,
#[ts(type = "string")] #[ts(type = "string")]
pub registry_url: Url, pub version_range: VersionRange,
#[ts(type = "string")]
pub version_spec: VersionRange,
pub config_satisfied: bool, pub config_satisfied: bool,
} }

View File

@@ -2,18 +2,21 @@ use std::collections::BTreeMap;
use std::time::Duration; use std::time::Duration;
use clap::Parser; use clap::Parser;
use imbl_value::InternedString;
use models::PackageId; use models::PackageId;
use patch_db::json_patch::merge; use patch_db::json_patch::merge;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use url::Url;
use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::config::{Config, ConfigSpec, ConfigureContext};
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::package::CurrentDependencies; use crate::db::model::package::CurrentDependencies;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::util::PathOrUrl;
use crate::Error; use crate::Error;
pub fn dependency<C: Context>() -> ParentHandler<C> { pub fn dependency<C: Context>() -> ParentHandler<C> {
@@ -42,6 +45,16 @@ impl Map for Dependencies {
pub struct DepInfo { pub struct DepInfo {
pub description: Option<String>, pub description: Option<String>,
pub optional: bool, pub optional: bool,
pub s9pk: Option<PathOrUrl>,
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DependencyMetadata {
#[ts(type = "string")]
pub title: InternedString,
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]

View File

@@ -554,33 +554,54 @@ pub struct InitProgressRes {
pub async fn init_progress(ctx: InitContext) -> Result<InitProgressRes, Error> { pub async fn init_progress(ctx: InitContext) -> Result<InitProgressRes, Error> {
let progress_tracker = ctx.progress.clone(); let progress_tracker = ctx.progress.clone();
let progress = progress_tracker.snapshot(); let progress = progress_tracker.snapshot();
let mut error = ctx.error.subscribe();
let guid = Guid::new(); let guid = Guid::new();
ctx.rpc_continuations ctx.rpc_continuations
.add( .add(
guid.clone(), guid.clone(),
RpcContinuation::ws( RpcContinuation::ws(
|mut ws| async move { |mut ws| async move {
if let Err(e) = async { let res = tokio::try_join!(
let mut stream = progress_tracker.stream(Some(Duration::from_millis(100))); async {
while let Some(progress) = stream.next().await { let mut stream =
ws.send(ws::Message::Text( progress_tracker.stream(Some(Duration::from_millis(100)));
serde_json::to_string(&progress) while let Some(progress) = stream.next().await {
.with_kind(ErrorKind::Serialization)?, ws.send(ws::Message::Text(
)) serde_json::to_string(&progress)
.await .with_kind(ErrorKind::Serialization)?,
.with_kind(ErrorKind::Network)?; ))
if progress.overall.is_complete() { .await
break; .with_kind(ErrorKind::Network)?;
if progress.overall.is_complete() {
break;
}
}
Ok::<_, Error>(())
},
async {
if let Some(e) = error
.wait_for(|e| e.is_some())
.await
.ok()
.and_then(|e| e.as_ref().map(|e| e.clone_output()))
{
Err::<(), _>(e)
} else {
Ok(())
} }
} }
);
ws.normal_close("complete").await?; if let Err(e) = ws
.close_result(res.map(|_| "complete").map_err(|e| {
Ok::<_, Error>(()) tracing::error!("error in init progress websocket: {e}");
} tracing::debug!("{e:?}");
.await e
}))
.await
{ {
tracing::error!("error in init progress websocket: {e}"); tracing::error!("error closing init progress websocket: {e}");
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
}, },

View File

@@ -111,6 +111,7 @@ impl std::fmt::Display for MinMax {
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct InstallParams { pub struct InstallParams {
#[ts(type = "string")] #[ts(type = "string")]
registry: Url, registry: Url,

View File

@@ -7,7 +7,6 @@ use serde::Deserialize;
use crate::context::RpcContext; use crate::context::RpcContext;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
#[serde(default)] #[serde(default)]
sync_db: bool, sync_db: bool,

View File

@@ -1,5 +1,8 @@
use std::cmp::min;
use std::future::Future; use std::future::Future;
use std::io::Cursor;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use async_compression::tokio::bufread::GzipEncoder; use async_compression::tokio::bufread::GzipEncoder;
@@ -8,36 +11,51 @@ use axum::extract::{self as x, Request};
use axum::response::Response; use axum::response::Response;
use axum::routing::{any, get, post}; use axum::routing::{any, get, post};
use axum::Router; use axum::Router;
use base64::display::Base64Display;
use digest::Digest; use digest::Digest;
use futures::future::ready; use futures::future::ready;
use http::header::ACCEPT_ENCODING; use http::header::{
ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH,
CONTENT_RANGE, CONTENT_TYPE, ETAG, RANGE,
};
use http::request::Parts as RequestParts; use http::request::Parts as RequestParts;
use http::{Method, StatusCode}; use http::{HeaderValue, Method, StatusCode};
use imbl_value::InternedString; use imbl_value::InternedString;
use include_dir::Dir; use include_dir::Dir;
use new_mime_guess::MimeGuess; use new_mime_guess::MimeGuess;
use openssl::hash::MessageDigest; use openssl::hash::MessageDigest;
use openssl::x509::X509; use openssl::x509::X509;
use rpc_toolkit::{Context, HttpServer, Server}; use rpc_toolkit::{Context, HttpServer, Server};
use tokio::io::BufReader; use sqlx::query;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use url::Url;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::hostname::Hostname; use crate::hostname::Hostname;
use crate::install::PKG_ARCHIVE_DIR;
use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::auth::{Auth, HasValidSession};
use crate::middleware::cors::Cors; use crate::middleware::cors::Cors;
use crate::middleware::db::SyncDb; use crate::middleware::db::SyncDb;
use crate::prelude::*;
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::rpc_continuations::{Guid, RpcContinuations}; use crate::rpc_continuations::{Guid, RpcContinuations};
use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk;
use crate::util::io::open_file; use crate::util::io::open_file;
use crate::{ use crate::util::net::SyncBody;
diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, use crate::util::serde::BASE64;
}; use crate::{diagnostic_api, init_api, install_api, main_api, setup_api};
const NOT_FOUND: &[u8] = b"Not Found"; const NOT_FOUND: &[u8] = b"Not Found";
const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed";
const NOT_AUTHORIZED: &[u8] = b"Not Authorized"; const NOT_AUTHORIZED: &[u8] = b"Not Authorized";
const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error";
const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"];
#[cfg(all(feature = "daemon", not(feature = "test")))] #[cfg(all(feature = "daemon", not(feature = "test")))]
const EMBEDDED_UIS: Dir<'_> = const EMBEDDED_UIS: Dir<'_> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static");
@@ -97,7 +115,7 @@ pub fn rpc_router<C: Context + Clone + AsRef<RpcContinuations>>(
fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> { fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
let (request_parts, _body) = req.into_parts(); let (request_parts, _body) = req.into_parts();
match &request_parts.method { match &request_parts.method {
&Method::GET => { &Method::GET | &Method::HEAD => {
let uri_path = ui_mode.path( let uri_path = ui_mode.path(
request_parts request_parts
.uri .uri
@@ -111,7 +129,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
.or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html"))); .or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html")));
if let Some(file) = file { if let Some(file) = file {
FileData::from_embedded(&request_parts, file).into_response(&request_parts) FileData::from_embedded(&request_parts, file)?.into_response(&request_parts)
} else { } else {
Ok(not_found()) Ok(not_found())
} }
@@ -161,14 +179,35 @@ pub fn init_ui_router(ctx: InitContext) -> Router {
} }
pub fn main_ui_router(ctx: RpcContext) -> Router { pub fn main_ui_router(ctx: RpcContext) -> Router {
rpc_router( rpc_router(ctx.clone(), {
ctx.clone(), let ctx = ctx.clone();
Server::new(move || ready(Ok(ctx.clone())), main_api::<RpcContext>()) Server::new(move || ready(Ok(ctx.clone())), main_api::<RpcContext>())
.middleware(Cors::new()) .middleware(Cors::new())
.middleware(Auth::new()) .middleware(Auth::new())
.middleware(SyncDb::new()), .middleware(SyncDb::new())
})
.route("/proxy/:url", {
let ctx = ctx.clone();
any(move |x::Path(url): x::Path<String>, request: Request| {
let ctx = ctx.clone();
async move {
proxy_request(ctx, request, url)
.await
.unwrap_or_else(server_error)
}
})
})
.nest("/s9pk", s9pk_router(ctx.clone()))
.route(
"/static/local-root-ca.crt",
get(move || {
let ctx = ctx.clone();
async move {
let account = ctx.account.read().await;
cert_send(&account.root_ca_cert, &account.hostname)
}
}),
) )
// TODO: cert
.fallback(any(|request: Request| async move { .fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error) serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
})) }))
@@ -179,29 +218,133 @@ pub fn refresher() -> Router {
let res = include_bytes!("./refresher.html"); let res = include_bytes!("./refresher.html");
FileData { FileData {
data: Body::from(&res[..]), data: Body::from(&res[..]),
content_range: None,
e_tag: None, e_tag: None,
encoding: None, encoding: None,
len: Some(res.len() as u64), len: Some(res.len() as u64),
mime: Some("text/html".into()), mime: Some("text/html".into()),
digest: None,
} }
.into_response(&request.into_parts().0) .into_response(&request.into_parts().0)
.unwrap_or_else(server_error) .unwrap_or_else(server_error)
})) }))
} }
async fn proxy_request(ctx: RpcContext, request: Request, url: String) -> Result<Response, Error> {
if_authorized(&ctx, request, |mut request| async {
for header in PROXY_STRIP_HEADERS {
request.headers_mut().remove(*header);
}
*request.uri_mut() = url.parse()?;
let request = request.map(|b| reqwest::Body::wrap_stream(SyncBody::from(b)));
let response = ctx.client.execute(request.try_into()?).await?;
Ok(Response::from(response).map(|b| Body::new(b)))
})
.await
}
fn s9pk_router(ctx: RpcContext) -> Router {
Router::new()
.route("/installed/:s9pk", {
let ctx = ctx.clone();
any(
|x::Path(s9pk): x::Path<String>, request: Request| async move {
if_authorized(&ctx, request, |request| async {
let (parts, _) = request.into_parts();
match FileData::from_path(
&parts,
&ctx.datadir
.join(PKG_ARCHIVE_DIR)
.join("installed")
.join(s9pk),
)
.await?
{
Some(file) => file.into_response(&parts),
None => Ok(not_found()),
}
})
.await
.unwrap_or_else(server_error)
},
)
})
.route("/installed/:s9pk/*path", {
let ctx = ctx.clone();
any(
|x::Path(s9pk): x::Path<String>,
x::Path(path): x::Path<PathBuf>,
x::Query(commitment): x::Query<Option<MerkleArchiveCommitment>>,
request: Request| async move {
if_authorized(&ctx, request, |request| async {
let s9pk = S9pk::deserialize(
&MultiCursorFile::from(
open_file(
ctx.datadir
.join(PKG_ARCHIVE_DIR)
.join("installed")
.join(s9pk),
)
.await?,
),
commitment.as_ref(),
)
.await?;
let (parts, _) = request.into_parts();
match FileData::from_s9pk(&parts, &s9pk, &path).await? {
Some(file) => file.into_response(&parts),
None => Ok(not_found()),
}
})
.await
.unwrap_or_else(server_error)
},
)
})
.route(
"/proxy/:url/*path",
any(
|x::Path((url, path)): x::Path<(Url, PathBuf)>,
x::RawQuery(query): x::RawQuery,
request: Request| async move {
if_authorized(&ctx, request, |request| async {
let s9pk = S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url).await?),
query
.as_deref()
.map(MerkleArchiveCommitment::from_query)
.and_then(|a| a.transpose())
.transpose()?
.as_ref(),
)
.await?;
let (parts, _) = request.into_parts();
match FileData::from_s9pk(&parts, &s9pk, &path).await? {
Some(file) => file.into_response(&parts),
None => Ok(not_found()),
}
})
.await
.unwrap_or_else(server_error)
},
),
)
}
async fn if_authorized< async fn if_authorized<
F: FnOnce() -> Fut, F: FnOnce(Request) -> Fut,
Fut: Future<Output = Result<Response, Error>> + Send + Sync, Fut: Future<Output = Result<Response, Error>> + Send,
>( >(
ctx: &RpcContext, ctx: &RpcContext,
parts: &RequestParts, request: Request,
f: F, f: F,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if let Err(e) = HasValidSession::from_header(parts.headers.get(http::header::COOKIE), ctx).await if let Err(e) =
HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await
{ {
Ok(unauthorized(e, parts.uri.path())) Ok(unauthorized(e, request.uri().path()))
} else { } else {
f().await f(request).await
} }
} }
@@ -268,44 +411,117 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result<Response, Error> {
.with_kind(ErrorKind::Network) .with_kind(ErrorKind::Network)
} }
fn parse_range(header: &HeaderValue, len: u64) -> Result<(u64, u64, u64), Error> {
let r = header
.to_str()
.with_kind(ErrorKind::Network)?
.trim()
.strip_prefix("bytes=")
.ok_or_else(|| Error::new(eyre!("invalid range units"), ErrorKind::InvalidRequest))?;
if r.contains(",") {
return Err(Error::new(
eyre!("multi-range requests are unsupported"),
ErrorKind::InvalidRequest,
));
}
if let Some((start, end)) = r.split_once("-").map(|(s, e)| (s.trim(), e.trim())) {
Ok((
if start.is_empty() {
0u64
} else {
start.parse()?
},
if end.is_empty() {
len - 1
} else {
min(end.parse()?, len - 1)
},
len,
))
} else {
Ok((len - r.trim().parse::<u64>()?, len - 1, len))
}
}
struct FileData { struct FileData {
data: Body, data: Body,
len: Option<u64>, len: Option<u64>,
content_range: Option<(u64, u64, u64)>,
encoding: Option<&'static str>, encoding: Option<&'static str>,
e_tag: Option<String>, e_tag: Option<String>,
mime: Option<InternedString>, mime: Option<InternedString>,
digest: Option<(&'static str, Vec<u8>)>,
} }
impl FileData { impl FileData {
fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self { fn from_embedded(
req: &RequestParts,
file: &'static include_dir::File<'static>,
) -> Result<Self, Error> {
let path = file.path(); let path = file.path();
let (encoding, data) = req let (encoding, data, len, content_range) = if let Some(range) = req.headers.get(RANGE) {
.headers let data = file.contents();
.get_all(ACCEPT_ENCODING) let (start, end, size) = parse_range(range, data.len() as u64)?;
.into_iter() let encoding = req
.filter_map(|h| h.to_str().ok()) .headers
.flat_map(|s| s.split(",")) .get_all(ACCEPT_ENCODING)
.filter_map(|s| s.split(";").next()) .into_iter()
.map(|s| s.trim()) .filter_map(|h| h.to_str().ok())
.fold((None, file.contents()), |acc, e| { .flat_map(|s| s.split(","))
if let Some(file) = (e == "br") .filter_map(|s| s.split(";").next())
.then_some(()) .map(|s| s.trim())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) .any(|e| e == "gzip")
{ .then_some("gzip");
(Some("br"), file.contents()) let data = if start > end {
} else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) &[]
.then_some(()) } else {
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) &data[(start as usize)..=(end as usize)]
{ };
(Some("gzip"), file.contents()) let (len, data) = if encoding == Some("gzip") {
} else { (
acc None,
} Body::from_stream(ReaderStream::new(GzipEncoder::new(Cursor::new(data)))),
}); )
} else {
(Some(data.len() as u64), Body::from(data))
};
(encoding, data, len, Some((start, end, size)))
} else {
let (encoding, data) = req
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.fold((None, file.contents()), |acc, e| {
if let Some(file) = (e == "br")
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display())))
{
(Some("br"), file.contents())
} else if let Some(file) = (e == "gzip" && acc.0 != Some("br"))
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display())))
{
(Some("gzip"), file.contents())
} else {
acc
}
});
(encoding, Body::from(data), Some(data.len() as u64), None)
};
Self { Ok(Self {
len: Some(data.len() as u64), len,
encoding, encoding,
data: data.into(), content_range,
data: if req.method == Method::HEAD {
Body::empty()
} else {
data
},
e_tag: file.metadata().map(|metadata| { e_tag: file.metadata().map(|metadata| {
e_tag( e_tag(
path, path,
@@ -323,11 +539,28 @@ impl FileData {
mime: MimeGuess::from_path(path) mime: MimeGuess::from_path(path)
.first() .first()
.map(|m| m.essence_str().into()), .map(|m| m.essence_str().into()),
digest: None,
})
}
fn encode<R: AsyncRead + Send + 'static>(
encoding: &mut Option<&str>,
data: R,
len: u64,
) -> (Option<u64>, Body) {
if *encoding == Some("gzip") {
(
None,
Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(data)))),
)
} else {
*encoding = None;
(Some(len), Body::from_stream(ReaderStream::new(data)))
} }
} }
async fn from_path(req: &RequestParts, path: &Path) -> Result<Self, Error> { async fn from_path(req: &RequestParts, path: &Path) -> Result<Option<Self>, Error> {
let encoding = req let mut encoding = req
.headers .headers
.get_all(ACCEPT_ENCODING) .get_all(ACCEPT_ENCODING)
.into_iter() .into_iter()
@@ -338,12 +571,23 @@ impl FileData {
.any(|e| e == "gzip") .any(|e| e == "gzip")
.then_some("gzip"); .then_some("gzip");
let file = open_file(path).await?; if tokio::fs::metadata(path).await.is_err() {
return Ok(None);
}
let mut file = open_file(path).await?;
let metadata = file let metadata = file
.metadata() .metadata()
.await .await
.with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?;
let content_range = req
.headers
.get(RANGE)
.map(|r| parse_range(r, metadata.len()))
.transpose()?;
let e_tag = Some(e_tag( let e_tag = Some(e_tag(
path, path,
format!( format!(
@@ -357,51 +601,123 @@ impl FileData {
.as_bytes(), .as_bytes(),
)); ));
let (len, data) = if encoding == Some("gzip") { let (len, data) = if let Some((start, end, _)) = content_range {
( let len = end + 1 - start;
None, file.seek(std::io::SeekFrom::Start(start)).await?;
Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))), Self::encode(&mut encoding, file.take(len), len)
)
} else { } else {
( Self::encode(&mut encoding, file, metadata.len())
Some(metadata.len()),
Body::from_stream(ReaderStream::new(file)),
)
}; };
Ok(Self { Ok(Some(Self {
data, data: if req.method == Method::HEAD {
Body::empty()
} else {
data
},
len, len,
content_range,
encoding, encoding,
e_tag, e_tag,
mime: MimeGuess::from_path(path) mime: MimeGuess::from_path(path)
.first() .first()
.map(|m| m.essence_str().into()), .map(|m| m.essence_str().into()),
}) digest: None,
}))
}
async fn from_s9pk<S: FileSource>(
req: &RequestParts,
s9pk: &S9pk<S>,
path: &Path,
) -> Result<Option<Self>, Error> {
let mut encoding = req
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.any(|e| e == "gzip")
.then_some("gzip");
let Some(file) = s9pk.as_archive().contents().get_path(path) else {
return Ok(None);
};
let Some(contents) = file.as_file() else {
return Ok(None);
};
let (digest, len) = if let Some((hash, len)) = file.hash() {
(Some(("blake3", hash.as_bytes().to_vec())), len)
} else {
(None, contents.size().await?)
};
let content_range = req
.headers
.get(RANGE)
.map(|r| parse_range(r, len))
.transpose()?;
let (len, data) = if let Some((start, end, _)) = content_range {
let len = end + 1 - start;
Self::encode(&mut encoding, contents.slice(start, len).await?, len)
} else {
Self::encode(&mut encoding, contents.reader().await?.take(len), len)
};
Ok(Some(Self {
data: if req.method == Method::HEAD {
Body::empty()
} else {
data
},
len,
content_range,
encoding,
e_tag: None,
mime: MimeGuess::from_path(path)
.first()
.map(|m| m.essence_str().into()),
digest,
}))
} }
fn into_response(self, req: &RequestParts) -> Result<Response, Error> { fn into_response(self, req: &RequestParts) -> Result<Response, Error> {
let mut builder = Response::builder(); let mut builder = Response::builder();
if let Some(mime) = self.mime { if let Some(mime) = self.mime {
builder = builder.header(http::header::CONTENT_TYPE, &*mime); builder = builder.header(CONTENT_TYPE, &*mime);
} }
if let Some(e_tag) = &self.e_tag { if let Some(e_tag) = &self.e_tag {
builder = builder.header(http::header::ETAG, &**e_tag); builder = builder
.header(ETAG, &**e_tag)
.header(CACHE_CONTROL, "public, max-age=21000000, immutable");
}
builder = builder.header(ACCEPT_RANGES, "bytes");
if let Some((start, end, size)) = self.content_range {
builder = builder
.header(CONTENT_RANGE, format!("bytes {start}-{end}/{size}"))
.status(StatusCode::PARTIAL_CONTENT);
}
if let Some((algorithm, digest)) = self.digest {
builder = builder.header(
"Repr-Digest",
format!("{algorithm}=:{}:", Base64Display::new(&digest, &BASE64)),
);
} }
builder = builder.header(
http::header::CACHE_CONTROL,
"public, max-age=21000000, immutable",
);
if req if req
.headers .headers
.get_all(http::header::CONNECTION) .get_all(CONNECTION)
.iter() .iter()
.flat_map(|s| s.to_str().ok()) .flat_map(|s| s.to_str().ok())
.flat_map(|s| s.split(",")) .flat_map(|s| s.split(","))
.any(|s| s.trim() == "keep-alive") .any(|s| s.trim() == "keep-alive")
{ {
builder = builder.header(http::header::CONNECTION, "keep-alive"); builder = builder.header(CONNECTION, "keep-alive");
} }
if self.e_tag.is_some() if self.e_tag.is_some()
@@ -411,14 +727,13 @@ impl FileData {
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
== self.e_tag.as_deref() == self.e_tag.as_deref()
{ {
builder = builder.status(StatusCode::NOT_MODIFIED); builder.status(StatusCode::NOT_MODIFIED).body(Body::empty())
builder.body(Body::empty())
} else { } else {
if let Some(len) = self.len { if let Some(len) = self.len {
builder = builder.header(http::header::CONTENT_LENGTH, len); builder = builder.header(CONTENT_LENGTH, len);
} }
if let Some(encoding) = self.encoding { if let Some(encoding) = self.encoding {
builder = builder.header(http::header::CONTENT_ENCODING, encoding); builder = builder.header(CONTENT_ENCODING, encoding);
} }
builder.body(self.data) builder.body(self.data)

View File

@@ -46,7 +46,7 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
.with_metadata("admin", Value::Bool(true)) .with_metadata("admin", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand("add", from_fn_async(cli_add_signer).no_display()) .subcommand("add", from_fn_async(cli_add_signer))
} }
impl Model<BTreeMap<Guid, SignerInfo>> { impl Model<BTreeMap<Guid, SignerInfo>> {
@@ -71,7 +71,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
.ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization))
} }
pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<(), Error> { pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> {
if let Some((guid, s)) = self if let Some((guid, s)) = self
.as_entries()? .as_entries()?
.into_iter() .into_iter()
@@ -89,7 +89,9 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
} }
self.insert(&Guid::new(), signer) let id = Guid::new();
self.insert(&id, signer)?;
Ok(id)
} }
} }
@@ -122,7 +124,7 @@ pub fn display_signers<T>(params: WithIoFormat<T>, signers: BTreeMap<Guid, Signe
table.print_tty(false).unwrap(); table.print_tty(false).unwrap();
} }
pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<(), Error> { pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid, Error> {
ctx.db ctx.db
.mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer))
.await .await
@@ -155,7 +157,7 @@ pub async fn cli_add_signer(
}, },
.. ..
}: HandlerArgs<CliContext, CliAddSignerParams>, }: HandlerArgs<CliContext, CliAddSignerParams>,
) -> Result<(), Error> { ) -> Result<Guid, Error> {
let signer = SignerInfo { let signer = SignerInfo {
name, name,
contact, contact,
@@ -165,15 +167,16 @@ pub async fn cli_add_signer(
TypedPatchDb::<RegistryDatabase>::load(PatchDb::open(database).await?) TypedPatchDb::<RegistryDatabase>::load(PatchDb::open(database).await?)
.await? .await?
.mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer))
.await?; .await
} else { } else {
ctx.call_remote::<RegistryContext>( from_value(
&parent_method.into_iter().chain(method).join("."), ctx.call_remote::<RegistryContext>(
to_value(&signer)?, &parent_method.into_iter().chain(method).join("."),
to_value(&signer)?,
)
.await?,
) )
.await?;
} }
Ok(())
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS)]

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, Utc};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
@@ -20,6 +21,8 @@ use crate::s9pk::S9pk;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct RegistryAsset<Commitment> { pub struct RegistryAsset<Commitment> {
#[ts(type = "string")]
pub published_at: DateTime<Utc>,
#[ts(type = "string")] #[ts(type = "string")]
pub url: Url, pub url: Url,
pub commitment: Commitment, pub commitment: Commitment,

View File

@@ -27,7 +27,6 @@ use crate::util::serde::Base64;
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig";
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
#[serde(default)] #[serde(default)]
admin: bool, admin: bool,
@@ -75,9 +74,7 @@ pub struct RegistryAdminLogRecord {
pub key: AnyVerifyingKey, pub key: AnyVerifyingKey,
} }
#[derive(Serialize, Deserialize)]
pub struct SignatureHeader { pub struct SignatureHeader {
#[serde(flatten)]
pub commitment: RequestCommitment, pub commitment: RequestCommitment,
pub signer: AnyVerifyingKey, pub signer: AnyVerifyingKey,
pub signature: AnySignature, pub signature: AnySignature,
@@ -93,14 +90,9 @@ impl SignatureHeader {
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
} }
pub fn from_header(header: &HeaderValue) -> Result<Self, Error> { pub fn from_header(header: &HeaderValue) -> Result<Self, Error> {
let url: Url = format!( let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
"http://localhost/?{}",
header.to_str().with_kind(ErrorKind::Utf8)?
)
.parse()?;
let query: BTreeMap<_, _> = url.query_pairs().collect();
Ok(Self { Ok(Self {
commitment: RequestCommitment::from_query(&url)?, commitment: RequestCommitment::from_query(&header)?,
signer: query.get("signer").or_not_found("signer")?.parse()?, signer: query.get("signer").or_not_found("signer")?.parse()?,
signature: query.get("signature").or_not_found("signature")?.parse()?, signature: query.get("signature").or_not_found("signature")?.parse()?,
}) })

View File

@@ -200,6 +200,19 @@ impl CallRemote<RegistryContext> for CliContext {
.send() .send()
.await?; .await?;
if !res.status().is_success() {
let status = res.status();
let txt = res.text().await?;
let mut res = Err(Error::new(
eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())),
ErrorKind::Network,
));
if !txt.is_empty() {
res = res.with_ctx(|_| (ErrorKind::Network, txt));
}
return res.map_err(From::from);
}
match res match res
.headers() .headers()
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
@@ -210,7 +223,7 @@ impl CallRemote<RegistryContext> for CliContext {
.with_kind(ErrorKind::Deserialization)? .with_kind(ErrorKind::Deserialization)?
.result .result
} }
_ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
} }
} }
} }
@@ -247,6 +260,19 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
.send() .send()
.await?; .await?;
if !res.status().is_success() {
let status = res.status();
let txt = res.text().await?;
let mut res = Err(Error::new(
eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())),
ErrorKind::Network,
));
if !txt.is_empty() {
res = res.with_ctx(|_| (ErrorKind::Network, txt));
}
return res.map_err(From::from);
}
match res match res
.headers() .headers()
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
@@ -257,7 +283,7 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
.with_kind(ErrorKind::Deserialization)? .with_kind(ErrorKind::Deserialization)?
.result .result
} }
_ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
} }
} }
} }

View File

@@ -54,12 +54,7 @@ impl DeviceInfo {
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 url: Url = format!( let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
"http://localhost/?{}",
header.to_str().with_kind(ErrorKind::ParseUrl)?
)
.parse()?;
let query: BTreeMap<_, _> = url.query_pairs().collect();
Ok(Self { Ok(Self {
os: OsInfo { os: OsInfo {
version: query version: query
@@ -151,7 +146,6 @@ impl From<&RpcContext> for HardwareInfo {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
#[serde(default)] #[serde(default)]
get_device_info: bool, get_device_info: bool,

View File

@@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
use axum::Router; use axum::Router;
use futures::future::ready; use futures::future::ready;
use imbl_value::InternedString;
use models::DataUrl; use models::DataUrl;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -16,7 +17,7 @@ use crate::registry::auth::Auth;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfoMiddleware; use crate::registry::device_info::DeviceInfoMiddleware;
use crate::registry::os::index::OsIndex; use crate::registry::os::index::OsIndex;
use crate::registry::package::index::PackageIndex; use crate::registry::package::index::{Category, PackageIndex};
use crate::registry::signer::SignerInfo; use crate::registry::signer::SignerInfo;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::util::serde::HandlerExtSerde; use crate::util::serde::HandlerExtSerde;
@@ -45,6 +46,7 @@ impl RegistryDatabase {}
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct FullIndex { pub struct FullIndex {
pub name: Option<String>,
pub icon: Option<DataUrl<'static>>, pub icon: Option<DataUrl<'static>>,
pub package: PackageIndex, pub package: PackageIndex,
pub os: OsIndex, pub os: OsIndex,
@@ -55,6 +57,25 @@ pub async fn get_full_index(ctx: RegistryContext) -> Result<FullIndex, Error> {
ctx.db.peek().await.into_index().de() ctx.db.peek().await.into_index().de()
} }
#[derive(Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RegistryInfo {
pub name: Option<String>,
pub icon: Option<DataUrl<'static>>,
#[ts(as = "BTreeMap::<String, Category>")]
pub categories: BTreeMap<InternedString, Category>,
}
pub async fn get_info(ctx: RegistryContext) -> Result<RegistryInfo, Error> {
let peek = ctx.db.peek().await.into_index();
Ok(RegistryInfo {
name: peek.as_name().de()?,
icon: peek.as_icon().de()?,
categories: peek.as_package().as_categories().de()?,
})
}
pub fn registry_api<C: Context>() -> ParentHandler<C> { pub fn registry_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
.subcommand( .subcommand(
@@ -63,6 +84,12 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
.with_display_serializable() .with_display_serializable()
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand(
"info",
from_fn_async(get_info)
.with_display_serializable()
.with_call_remote::<CliContext>(),
)
.subcommand("os", os::os_api::<C>()) .subcommand("os", os::os_api::<C>())
.subcommand("package", package::package_api::<C>()) .subcommand("package", package::package_api::<C>())
.subcommand("admin", admin::admin_api::<C>()) .subcommand("admin", admin::admin_api::<C>())

View File

@@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap};
use std::panic::UnwindSafe; use std::panic::UnwindSafe;
use std::path::PathBuf; use std::path::PathBuf;
use chrono::Utc;
use clap::Parser; use clap::Parser;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools; use itertools::Itertools;
@@ -12,7 +13,7 @@ use url::Url;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker}; use crate::progress::FullProgressTracker;
use crate::registry::asset::RegistryAsset; use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo; use crate::registry::os::index::OsVersionInfo;
@@ -33,19 +34,19 @@ pub fn add_api<C: Context>() -> ParentHandler<C> {
.subcommand( .subcommand(
"iso", "iso",
from_fn_async(add_iso) from_fn_async(add_iso)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand( .subcommand(
"img", "img",
from_fn_async(add_img) from_fn_async(add_img)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand( .subcommand(
"squashfs", "squashfs",
from_fn_async(add_squashfs) from_fn_async(add_squashfs)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
} }
@@ -107,6 +108,7 @@ async fn add_asset(
) )
.upsert(&platform, || { .upsert(&platform, || {
Ok(RegistryAsset { Ok(RegistryAsset {
published_at: Utc::now(),
url, url,
commitment: commitment.clone(), commitment: commitment.clone(),
signatures: HashMap::new(), signatures: HashMap::new(),

View File

@@ -30,19 +30,19 @@ pub fn sign_api<C: Context>() -> ParentHandler<C> {
.subcommand( .subcommand(
"iso", "iso",
from_fn_async(sign_iso) from_fn_async(sign_iso)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand( .subcommand(
"img", "img",
from_fn_async(sign_img) from_fn_async(sign_img)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand( .subcommand(
"squashfs", "squashfs",
from_fn_async(sign_squashfs) from_fn_async(sign_squashfs)
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_cli(), .no_cli(),
) )
} }

View File

@@ -25,7 +25,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
"add", "add",
from_fn_async(add_version) from_fn_async(add_version)
.with_metadata("admin", Value::Bool(true)) .with_metadata("admin", Value::Bool(true))
.with_metadata("getSigner", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true))
.no_display() .no_display()
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -121,7 +121,7 @@ pub async fn remove_version(
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct GetVersionParams { pub struct GetOsVersionParams {
#[ts(type = "string | null")] #[ts(type = "string | null")]
#[arg(long = "src")] #[arg(long = "src")]
pub source: Option<VersionString>, pub source: Option<VersionString>,
@@ -138,12 +138,12 @@ pub struct GetVersionParams {
pub async fn get_version( pub async fn get_version(
ctx: RegistryContext, ctx: RegistryContext,
GetVersionParams { GetOsVersionParams {
source, source,
target, target,
server_id, server_id,
arch, arch,
}: GetVersionParams, }: GetOsVersionParams,
) -> Result<BTreeMap<VersionString, OsVersionInfo>, Error> { ) -> Result<BTreeMap<VersionString, OsVersionInfo>, Error> {
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) {
let created_at = Utc::now(); let created_at = Utc::now();

View File

@@ -11,13 +11,14 @@ use url::Url;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::FullProgressTracker; use crate::progress::{FullProgressTracker, ProgressTrackerWriter};
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::package::index::PackageVersionInfo; use crate::registry::package::index::PackageVersionInfo;
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::registry::signer::sign::ed25519::Ed25519; use crate::registry::signer::sign::ed25519::Ed25519;
use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::util::io::TrackingIO; use crate::util::io::TrackingIO;
@@ -126,13 +127,16 @@ pub async fn cli_add_package(
sign_phase.complete(); sign_phase.complete();
verify_phase.start(); verify_phase.start();
let mut src = S9pk::deserialize( let source = HttpSource::new(ctx.client.clone(), url.clone()).await?;
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), let len = source.size().await;
Some(&commitment), let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
) if let Some(len) = len {
.await?; verify_phase.set_total(len);
src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) }
let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase);
src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true)
.await?; .await?;
let (_, mut verify_phase) = verify_writer.into_inner();
verify_phase.complete(); verify_phase.complete();
index_phase.start(); index_phase.start();
@@ -140,7 +144,7 @@ pub async fn cli_add_package(
&parent_method.into_iter().chain(method).join("."), &parent_method.into_iter().chain(method).join("."),
imbl_value::json!({ imbl_value::json!({
"url": &url, "url": &url,
"signature": signature, "signature": AnySignature::Ed25519(signature),
"commitment": commitment, "commitment": commitment,
}), }),
) )

View File

@@ -21,6 +21,7 @@ use crate::util::VersionString;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub enum PackageDetailLevel { pub enum PackageDetailLevel {
None,
Short, Short,
Full, Full,
} }
@@ -50,7 +51,9 @@ pub struct GetPackageParams {
#[arg(skip)] #[arg(skip)]
#[serde(rename = "__device_info")] #[serde(rename = "__device_info")]
pub device_info: Option<DeviceInfo>, pub device_info: Option<DeviceInfo>,
pub other_versions: Option<PackageDetailLevel>, #[serde(default)]
#[arg(default_value = "none")]
pub other_versions: PackageDetailLevel,
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS)]
@@ -126,7 +129,6 @@ fn get_matching_models<'a>(
db: &'a Model<PackageIndex>, db: &'a Model<PackageIndex>,
GetPackageParams { GetPackageParams {
id, id,
version,
source_version, source_version,
device_info, device_info,
.. ..
@@ -148,22 +150,18 @@ fn get_matching_models<'a>(
.into_iter() .into_iter()
.map(|(v, info)| { .map(|(v, info)| {
Ok::<_, Error>( Ok::<_, Error>(
if version if source_version.as_ref().map_or(Ok(true), |source_version| {
Ok::<_, Error>(
source_version.satisfies(
&info
.as_source_version()
.de()?
.unwrap_or(VersionRange::any()),
),
)
})? && device_info
.as_ref() .as_ref()
.map_or(true, |version| v.satisfies(version)) .map_or(Ok(true), |device_info| info.works_for_device(device_info))?
&& source_version.as_ref().map_or(Ok(true), |source_version| {
Ok::<_, Error>(
source_version.satisfies(
&info
.as_source_version()
.de()?
.unwrap_or(VersionRange::any()),
),
)
})?
&& device_info
.as_ref()
.map_or(Ok(true), |device_info| info.works_for_device(device_info))?
{ {
Some((k.clone(), ExtendedVersion::from(v), info)) Some((k.clone(), ExtendedVersion::from(v), info))
} else { } else {
@@ -187,24 +185,27 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
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 mut package_best = best.remove(&id).unwrap_or_default(); let package_best = best.entry(id.clone()).or_default();
let mut package_other = other.remove(&id).unwrap_or_default(); let package_other = other.entry(id.clone()).or_default();
for worse_version in package_best if params
.keys() .version
.filter(|k| ***k < version) .as_ref()
.cloned() .map_or(true, |v| version.satisfies(v))
.collect_vec() && package_best.keys().all(|k| !(**k > version))
{ {
if let Some(info) = package_best.remove(&worse_version) { for worse_version in package_best
package_other.insert(worse_version, info); .keys()
.filter(|k| ***k < version)
.cloned()
.collect_vec()
{
if let Some(info) = package_best.remove(&worse_version) {
package_other.insert(worse_version, info);
}
} }
}
if package_best.keys().all(|k| !(**k > version)) {
package_best.insert(version.into(), info); package_best.insert(version.into(), info);
} } else {
best.insert(id.clone(), package_best); package_other.insert(version.into(), info);
if params.other_versions.is_some() {
other.insert(id.clone(), package_other);
} }
} }
if let Some(id) = params.id { if let Some(id) = params.id {
@@ -224,12 +225,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.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 {
None => to_value(&GetPackageResponse { PackageDetailLevel::None => to_value(&GetPackageResponse {
categories, categories,
best, best,
other_versions: None, other_versions: None,
}), }),
Some(PackageDetailLevel::Short) => to_value(&GetPackageResponse { PackageDetailLevel::Short => to_value(&GetPackageResponse {
categories, categories,
best, best,
other_versions: Some( other_versions: Some(
@@ -239,7 +240,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.try_collect()?, .try_collect()?,
), ),
}), }),
Some(PackageDetailLevel::Full) => to_value(&GetPackageResponseFull { PackageDetailLevel::Full => to_value(&GetPackageResponseFull {
categories, categories,
best, best,
other_versions: other other_versions: other
@@ -250,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
} }
} else { } else {
match params.other_versions { match params.other_versions {
None => to_value( PackageDetailLevel::None => to_value(
&best &best
.into_iter() .into_iter()
.map(|(id, best)| { .map(|(id, best)| {
@@ -276,7 +277,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
}) })
.try_collect::<_, GetPackagesResponse, _>()?, .try_collect::<_, GetPackagesResponse, _>()?,
), ),
Some(PackageDetailLevel::Short) => to_value( PackageDetailLevel::Short => to_value(
&best &best
.into_iter() .into_iter()
.map(|(id, best)| { .map(|(id, best)| {
@@ -310,7 +311,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
}) })
.try_collect::<_, GetPackagesResponse, _>()?, .try_collect::<_, GetPackagesResponse, _>()?,
), ),
Some(PackageDetailLevel::Full) => to_value( PackageDetailLevel::Full => to_value(
&best &best
.into_iter() .into_iter()
.map(|(id, best)| { .map(|(id, best)| {
@@ -354,7 +355,7 @@ pub fn display_package_info(
} }
if let Some(_) = params.rest.id { if let Some(_) = params.rest.id {
if params.rest.other_versions == Some(PackageDetailLevel::Full) { if params.rest.other_versions == 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!();
@@ -366,7 +367,7 @@ pub fn display_package_info(
} }
} }
} else { } else {
if params.rest.other_versions == Some(PackageDetailLevel::Full) { if params.rest.other_versions == 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)?;

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use chrono::Utc;
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{DataUrl, PackageId, VersionString}; use models::{DataUrl, PackageId, VersionString};
@@ -15,7 +16,7 @@ use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment
use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey};
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::s9pk::git_hash::GitHash; use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::{Description, HardwareRequirements}; use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
@@ -49,12 +50,25 @@ pub struct Category {
pub description: Description, pub description: Description,
} }
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DependencyMetadata {
#[ts(type = "string | null")]
pub title: Option<InternedString>,
pub icon: Option<DataUrl<'static>>,
pub description: Option<String>,
pub optional: bool,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct PackageVersionInfo { pub struct PackageVersionInfo {
pub title: String, #[ts(type = "string")]
pub title: InternedString,
pub icon: DataUrl<'static>, pub icon: DataUrl<'static>,
pub description: Description, pub description: Description,
pub release_notes: String, pub release_notes: String,
@@ -70,6 +84,10 @@ pub struct PackageVersionInfo {
pub support_site: Url, pub support_site: Url,
#[ts(type = "string")] #[ts(type = "string")]
pub marketing_site: Url, pub marketing_site: Url,
#[ts(type = "string | null")]
pub donation_url: Option<Url>,
pub alerts: Alerts,
pub dependency_metadata: BTreeMap<PackageId, DependencyMetadata>,
#[ts(type = "string")] #[ts(type = "string")]
pub os_version: Version, pub os_version: Version,
pub hardware_requirements: HardwareRequirements, pub hardware_requirements: HardwareRequirements,
@@ -80,6 +98,19 @@ pub struct PackageVersionInfo {
impl PackageVersionInfo { impl PackageVersionInfo {
pub async fn from_s9pk<S: FileSource + Clone>(s9pk: &S9pk<S>, url: Url) -> Result<Self, Error> { pub async fn from_s9pk<S: FileSource + Clone>(s9pk: &S9pk<S>, url: Url) -> Result<Self, Error> {
let manifest = s9pk.as_manifest(); let manifest = s9pk.as_manifest();
let mut dependency_metadata = BTreeMap::new();
for (id, info) in &manifest.dependencies.0 {
let metadata = s9pk.dependency_metadata(id).await?;
dependency_metadata.insert(
id.clone(),
DependencyMetadata {
title: metadata.map(|m| m.title),
icon: s9pk.dependency_icon_data_url(id).await?,
description: info.description.clone(),
optional: info.optional,
},
);
}
Ok(Self { Ok(Self {
title: manifest.title.clone(), title: manifest.title.clone(),
icon: s9pk.icon_data_url().await?, icon: s9pk.icon_data_url().await?,
@@ -91,10 +122,14 @@ impl PackageVersionInfo {
upstream_repo: manifest.upstream_repo.clone(), upstream_repo: manifest.upstream_repo.clone(),
support_site: manifest.support_site.clone(), support_site: manifest.support_site.clone(),
marketing_site: manifest.marketing_site.clone(), marketing_site: manifest.marketing_site.clone(),
donation_url: manifest.donation_url.clone(),
alerts: manifest.alerts.clone(),
dependency_metadata,
os_version: manifest.os_version.clone(), os_version: manifest.os_version.clone(),
hardware_requirements: manifest.hardware_requirements.clone(), hardware_requirements: manifest.hardware_requirements.clone(),
source_version: None, // TODO source_version: None, // TODO
s9pk: RegistryAsset { s9pk: RegistryAsset {
published_at: Utc::now(),
url, url,
commitment: s9pk.as_archive().commitment().await?, commitment: s9pk.as_archive().commitment().await?,
signatures: [( signatures: [(
@@ -114,8 +149,11 @@ impl PackageVersionInfo {
table.add_row(row![bc => &self.title]); table.add_row(row![bc => &self.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.release_notes]);
table.add_row(row![br -> "ABOUT", &self.description.short]); table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]);
table.add_row(row![br -> "DESCRIPTION", &self.description.long]); table.add_row(row![
br -> "DESCRIPTION",
&textwrap::wrap(&self.description.long, 80).join("\n")
]);
table.add_row(row![br -> "GIT HASH", AsRef::<str>::as_ref(&self.git_hash)]); table.add_row(row![br -> "GIT HASH", AsRef::<str>::as_ref(&self.git_hash)]);
table.add_row(row![br -> "LICENSE", &self.license]); table.add_row(row![br -> "LICENSE", &self.license]);
table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]);

View File

@@ -16,14 +16,21 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_display_serializable() .with_display_serializable()
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand("add", from_fn_async(add::add_package).no_cli()) .subcommand(
"add",
from_fn_async(add::add_package)
.with_metadata("get_signer", Value::Bool(true))
.no_cli(),
)
.subcommand("add", from_fn_async(add::cli_add_package).no_display()) .subcommand("add", from_fn_async(add::cli_add_package).no_display())
.subcommand( .subcommand(
"get", "get",
from_fn_async(get::get_package) from_fn_async(get::get_package)
.with_metadata("get_device_info", Value::Bool(true))
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| {
get::display_package_info(handle.params, result) get::display_package_info(handle.params, result)
}), })
.with_call_remote::<CliContext>(),
) )
} }

View File

@@ -20,6 +20,35 @@ pub struct MerkleArchiveCommitment {
#[ts(type = "number")] #[ts(type = "number")]
pub root_maxsize: u64, pub root_maxsize: u64,
} }
impl MerkleArchiveCommitment {
pub fn from_query(query: &str) -> Result<Option<Self>, Error> {
let mut root_sighash = None;
let mut root_maxsize = None;
for (k, v) in form_urlencoded::parse(dbg!(query).as_bytes()) {
match &*k {
"rootSighash" => {
root_sighash = Some(dbg!(v).parse()?);
}
"rootMaxsize" => {
root_maxsize = Some(v.parse()?);
}
_ => (),
}
}
if root_sighash.is_some() || root_maxsize.is_some() {
Ok(Some(Self {
root_sighash: root_sighash
.or_not_found("rootSighash required if rootMaxsize specified")
.with_kind(ErrorKind::InvalidRequest)?,
root_maxsize: root_maxsize
.or_not_found("rootMaxsize required if rootSighash specified")
.with_kind(ErrorKind::InvalidRequest)?,
}))
} else {
Ok(None)
}
}
}
impl Digestable for MerkleArchiveCommitment { impl Digestable for MerkleArchiveCommitment {
fn update<D: Update>(&self, digest: &mut D) { fn update<D: Update>(&self, digest: &mut D) {
digest.update(&*self.root_sighash); digest.update(&*self.root_sighash);

View File

@@ -5,6 +5,7 @@ use axum::body::Body;
use axum::extract::Request; use axum::extract::Request;
use digest::Update; use digest::Update;
use futures::TryStreamExt; use futures::TryStreamExt;
use http::HeaderValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use tokio_util::io::StreamReader; use tokio_util::io::StreamReader;
@@ -37,8 +38,8 @@ impl RequestCommitment {
.append_pair("size", &self.size.to_string()) .append_pair("size", &self.size.to_string())
.append_pair("blake3", &self.blake3.to_string()); .append_pair("blake3", &self.blake3.to_string());
} }
pub fn from_query(url: &Url) -> Result<Self, Error> { pub fn from_query(query: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = url.query_pairs().collect(); let query: BTreeMap<_, _> = form_urlencoded::parse(query.as_bytes()).collect();
Ok(Self { Ok(Self {
timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?, timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?,
nonce: query.get("nonce").or_not_found("nonce")?.parse()?, nonce: query.get("nonce").or_not_found("nonce")?.parse()?,

View File

@@ -1,4 +1,3 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
@@ -7,16 +6,16 @@ use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::merkle_archive::Entry; use crate::s9pk::merkle_archive::Entry;
/// An object for tracking the files expected to be in an s9pk /// An object for tracking the files expected to be in an s9pk
pub struct Expected<'a, T> { pub struct Expected<'a, T> {
keep: DirectoryContents<()>, keep: DirectoryContents<()>,
dir: &'a DirectoryContents<T>, dir: &'a DirectoryContents<T>,
} }
impl<'a, T> Expected<'a, T> { impl<'a, T> Expected<'a, T> {
pub fn new(dir: &'a DirectoryContents<T>,) -> Self { pub fn new(dir: &'a DirectoryContents<T>) -> Self {
Self { Self {
keep: DirectoryContents::new(), keep: DirectoryContents::new(),
dir dir,
} }
} }
} }
@@ -42,22 +41,23 @@ impl<'a, T: Clone> Expected<'a, T> {
path: impl AsRef<Path>, path: impl AsRef<Path>,
mut valid_extension: impl FnMut(Option<&OsStr>) -> bool, mut valid_extension: impl FnMut(Option<&OsStr>) -> bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (dir, stem) = if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { let (dir, stem) =
( if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) {
self.dir (
.get_path(parent) self.dir
.and_then(|e| e.as_directory()) .get_path(parent)
.ok_or_else(|| { .and_then(|e| e.as_directory())
Error::new( .ok_or_else(|| {
eyre!("directory {} missing from archive", parent.display()), Error::new(
ErrorKind::ParseS9pk, eyre!("directory {} missing from archive", parent.display()),
) ErrorKind::ParseS9pk,
})?, )
path.as_ref().strip_prefix(parent).unwrap(), })?,
) path.as_ref().strip_prefix(parent).unwrap(),
} else { )
(self.dir, path.as_ref()) } else {
}; (self.dir, path.as_ref())
};
let name = dir let name = dir
.with_stem(&stem.as_os_str().to_string_lossy()) .with_stem(&stem.as_os_str().to_string_lossy())
.filter(|(_, e)| e.as_file().is_some()) .filter(|(_, e)| e.as_file().is_some())
@@ -69,7 +69,7 @@ impl<'a, T: Clone> Expected<'a, T> {
), ),
ErrorKind::ParseS9pk, ErrorKind::ParseS9pk,
)), )),
|acc, (name, _)| |acc, (name, _)|
if valid_extension(Path::new(&*name).extension()) { if valid_extension(Path::new(&*name).extension()) {
match acc { match acc {
Ok(_) => Err(Error::new( Ok(_) => Err(Error::new(
@@ -96,8 +96,10 @@ impl<'a, T: Clone> Expected<'a, T> {
pub struct Filter(DirectoryContents<()>); pub struct Filter(DirectoryContents<()>);
impl Filter { impl Filter {
pub fn keep_checked<T: FileSource + Clone>(&self, dir: &mut DirectoryContents<T>) -> Result<(), Error> { pub fn keep_checked<T: FileSource + Clone>(
&self,
dir: &mut DirectoryContents<T>,
) -> Result<(), Error> {
dir.filter(|path| self.0.get_path(path).is_some()) dir.filter(|path| self.0.get_path(path).is_some())
} }
} }

View File

@@ -233,6 +233,10 @@ impl<S> Entry<S> {
_ => None, _ => None,
} }
} }
pub fn expect_file(&self) -> Result<&FileContents<S>, Error> {
self.as_file()
.ok_or_else(|| Error::new(eyre!("not a file"), ErrorKind::ParseS9pk))
}
pub fn as_directory(&self) -> Option<&DirectoryContents<S>> { pub fn as_directory(&self) -> Option<&DirectoryContents<S>> {
match self.as_contents() { match self.as_contents() {
EntryContents::Directory(d) => Some(d), EntryContents::Directory(d) => Some(d),

View File

@@ -1,3 +1,5 @@
use std::cmp::min;
use std::io::SeekFrom;
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@@ -6,7 +8,7 @@ use blake3::Hash;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::{Future, FutureExt}; use futures::{Future, FutureExt};
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, Take};
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::merkle_archive::hash::VerifyingWriter; use crate::s9pk::merkle_archive::hash::VerifyingWriter;
@@ -17,8 +19,14 @@ pub mod multi_cursor_file;
pub trait FileSource: Send + Sync + Sized + 'static { pub trait FileSource: Send + Sync + Sized + 'static {
type Reader: AsyncRead + Unpin + Send; type Reader: AsyncRead + Unpin + Send;
type SliceReader: AsyncRead + Unpin + Send;
fn size(&self) -> impl Future<Output = Result<u64, Error>> + Send; fn size(&self) -> impl Future<Output = Result<u64, Error>> + Send;
fn reader(&self) -> impl Future<Output = Result<Self::Reader, Error>> + Send; fn reader(&self) -> impl Future<Output = Result<Self::Reader, Error>> + Send;
fn slice(
&self,
position: u64,
size: u64,
) -> impl Future<Output = Result<Self::SliceReader, Error>> + Send;
fn copy<W: AsyncWrite + Unpin + Send + ?Sized>( fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(
&self, &self,
w: &mut W, w: &mut W,
@@ -65,12 +73,16 @@ pub trait FileSource: Send + Sync + Sized + 'static {
impl<T: FileSource> FileSource for Arc<T> { impl<T: FileSource> FileSource for Arc<T> {
type Reader = T::Reader; type Reader = T::Reader;
type SliceReader = T::SliceReader;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
self.deref().size().await self.deref().size().await
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
self.deref().reader().await self.deref().reader().await
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
self.deref().slice(position, size).await
}
async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> { async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> {
self.deref().copy(w).await self.deref().copy(w).await
} }
@@ -95,12 +107,16 @@ impl DynFileSource {
} }
impl FileSource for DynFileSource { impl FileSource for DynFileSource {
type Reader = Box<dyn AsyncRead + Unpin + Send>; type Reader = Box<dyn AsyncRead + Unpin + Send>;
type SliceReader = Box<dyn AsyncRead + Unpin + Send>;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
self.0.size().await self.0.size().await
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
self.0.reader().await self.0.reader().await
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
self.0.slice(position, size).await
}
async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>( async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(
&self, &self,
mut w: &mut W, mut w: &mut W,
@@ -123,6 +139,11 @@ impl FileSource for DynFileSource {
trait DynableFileSource: Send + Sync + 'static { trait DynableFileSource: Send + Sync + 'static {
async fn size(&self) -> Result<u64, Error>; async fn size(&self) -> Result<u64, Error>;
async fn reader(&self) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error>; async fn reader(&self) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error>;
async fn slice(
&self,
position: u64,
size: u64,
) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error>;
async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error>; async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error>;
async fn copy_verify( async fn copy_verify(
&self, &self,
@@ -139,6 +160,13 @@ impl<T: FileSource> DynableFileSource for T {
async fn reader(&self) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error> { async fn reader(&self) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error> {
Ok(Box::new(FileSource::reader(self).await?)) Ok(Box::new(FileSource::reader(self).await?))
} }
async fn slice(
&self,
position: u64,
size: u64,
) -> Result<Box<dyn AsyncRead + Unpin + Send>, Error> {
Ok(Box::new(FileSource::slice(self, position, size).await?))
}
async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error> { async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error> {
FileSource::copy(self, w).await FileSource::copy(self, w).await
} }
@@ -156,22 +184,34 @@ impl<T: FileSource> DynableFileSource for T {
impl FileSource for PathBuf { impl FileSource for PathBuf {
type Reader = File; type Reader = File;
type SliceReader = Take<Self::Reader>;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
Ok(tokio::fs::metadata(self).await?.len()) Ok(tokio::fs::metadata(self).await?.len())
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
Ok(open_file(self).await?) Ok(open_file(self).await?)
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
let mut r = FileSource::reader(self).await?;
r.seek(SeekFrom::Start(position)).await?;
Ok(r.take(size))
}
} }
impl FileSource for Arc<[u8]> { impl FileSource for Arc<[u8]> {
type Reader = std::io::Cursor<Self>; type Reader = std::io::Cursor<Self>;
type SliceReader = Take<Self::Reader>;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
Ok(self.len() as u64) Ok(self.len() as u64)
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
Ok(std::io::Cursor::new(self.clone())) Ok(std::io::Cursor::new(self.clone()))
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
let mut r = FileSource::reader(self).await?;
r.seek(SeekFrom::Start(position)).await?;
Ok(r.take(size))
}
async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> { async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> {
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@@ -272,12 +312,18 @@ pub struct Section<S> {
} }
impl<S: ArchiveSource> FileSource for Section<S> { impl<S: ArchiveSource> FileSource for Section<S> {
type Reader = S::FetchReader; type Reader = S::FetchReader;
type SliceReader = S::FetchReader;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
Ok(self.size) Ok(self.size)
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
self.source.fetch(self.position, self.size).await self.source.fetch(self.position, self.size).await
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
self.source
.fetch(self.position + position, min(size, self.size))
.await
}
async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> { async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(&self, w: &mut W) -> Result<(), Error> {
self.source.copy_to(self.position, self.size, w).await self.source.copy_to(self.position, self.size, w).await
} }
@@ -342,12 +388,16 @@ impl<S: FileSource> From<TmpSource<S>> for DynFileSource {
impl<S: FileSource> FileSource for TmpSource<S> { impl<S: FileSource> FileSource for TmpSource<S> {
type Reader = <S as FileSource>::Reader; type Reader = <S as FileSource>::Reader;
type SliceReader = <S as FileSource>::SliceReader;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
self.source.size().await self.source.size().await
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
self.source.reader().await self.source.reader().await
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
self.source.slice(position, size).await
}
async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>( async fn copy<W: AsyncWrite + Unpin + Send + ?Sized>(
&self, &self,
mut w: &mut W, mut w: &mut W,

View File

@@ -15,14 +15,36 @@ use crate::s9pk::v2::pack::ImageConfig;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::io::{create_file, open_file, TmpDir};
use crate::util::serde::{apply_expr, HandlerExtSerde}; use crate::util::serde::{apply_expr, HandlerExtSerde};
use crate::util::Apply;
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
pub fn s9pk() -> ParentHandler<CliContext> { pub fn s9pk() -> ParentHandler<CliContext> {
ParentHandler::new() ParentHandler::new()
.subcommand("pack", from_fn_async(super::v2::pack::pack).no_display()) .subcommand("pack", from_fn_async(super::v2::pack::pack).no_display())
.subcommand(
"list-ingredients",
from_fn_async(super::v2::pack::list_ingredients).with_custom_display_fn(
|_, ingredients| {
ingredients
.into_iter()
.map(Some)
.apply(|i| itertools::intersperse(i, None))
.for_each(|i| {
if let Some(p) = i {
print!("{}", p.display())
} else {
print!(" ")
}
});
println!();
Ok(())
},
),
)
.subcommand("edit", edit()) .subcommand("edit", edit())
.subcommand("inspect", inspect()) .subcommand("inspect", inspect())
.subcommand("convert", from_fn_async(convert).no_display())
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser)]
@@ -193,3 +215,17 @@ async fn inspect_manifest(
.await?; .await?;
Ok(s9pk.as_manifest().clone()) Ok(s9pk.as_manifest().clone())
} }
async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
let mut s9pk = super::load(
MultiCursorFile::from(open_file(&s9pk_path).await?),
|| ctx.developer_key().cloned(),
None,
)
.await?;
let tmp_path = s9pk_path.with_extension("s9pk.tmp");
s9pk.serialize(&mut create_file(&tmp_path).await?, true)
.await?;
tokio::fs::rename(tmp_path, s9pk_path).await?;
Ok(())
}

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@@ -199,8 +199,9 @@ impl From<ManifestV1> for Manifest {
let default_url = value.upstream_repo.clone(); let default_url = value.upstream_repo.clone();
Self { Self {
id: value.id, id: value.id,
title: value.title, title: value.title.into(),
version: ExtendedVersion::from(value.version).into(), version: ExtendedVersion::from(value.version).into(),
satisfies: BTreeSet::new(),
release_notes: value.release_notes, release_notes: value.release_notes,
license: value.license.into(), license: value.license.into(),
wrapper_repo: value.wrapper_repo, wrapper_repo: value.wrapper_repo,
@@ -233,6 +234,7 @@ impl From<ManifestV1> for Manifest {
DepInfo { DepInfo {
description: value.description, description: value.description,
optional: !value.requirement.required(), optional: !value.requirement.required(),
s9pk: None,
}, },
) )
}) })

View File

@@ -31,8 +31,10 @@ fn current_version() -> Version {
#[ts(export)] #[ts(export)]
pub struct Manifest { pub struct Manifest {
pub id: PackageId, pub id: PackageId,
pub title: String, #[ts(type = "string")]
pub title: InternedString,
pub version: VersionString, pub version: VersionString,
pub satisfies: BTreeSet<VersionString>,
pub release_notes: String, pub release_notes: String,
#[ts(type = "string")] #[ts(type = "string")]
pub license: InternedString, // type of license pub license: InternedString, // type of license
@@ -81,6 +83,15 @@ impl Manifest {
expected.check_file("LICENSE.md")?; expected.check_file("LICENSE.md")?;
expected.check_file("instructions.md")?; expected.check_file("instructions.md")?;
expected.check_file("javascript.squashfs")?; expected.check_file("javascript.squashfs")?;
for (dependency, _) in &self.dependencies.0 {
let dep_path = Path::new("dependencies").join(dependency);
let _ = expected.check_file(dep_path.join("metadata.json"));
let _ = expected.check_stem(dep_path.join("icon"), |ext| {
ext.and_then(|e| e.to_str())
.and_then(mime)
.map_or(false, |mime| mime.starts_with("image/"))
});
}
for assets in &self.assets { for assets in &self.assets {
expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?;
} }
@@ -148,7 +159,7 @@ impl Manifest {
#[ts(export)] #[ts(export)]
pub struct HardwareRequirements { pub struct HardwareRequirements {
#[serde(default)] #[serde(default)]
#[ts(type = "{ [key: string]: string }")] // TODO more specific key #[ts(type = "{ device?: string, processor?: string }")]
pub device: BTreeMap<String, Regex>, pub device: BTreeMap<String, Regex>,
#[ts(type = "number | null")] #[ts(type = "number | null")]
pub ram: Option<u64>, pub ram: Option<u64>,

View File

@@ -6,10 +6,10 @@ use imbl_value::InternedString;
use models::{mime, DataUrl, PackageId}; use models::{mime, DataUrl, PackageId};
use tokio::fs::File; use tokio::fs::File;
use crate::dependencies::DependencyMetadata;
use crate::prelude::*; use crate::prelude::*;
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::s9pk::manifest::Manifest; use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::file_contents::FileContents;
use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::sink::Sink;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::{ use crate::s9pk::merkle_archive::source::{
@@ -18,6 +18,7 @@ use crate::s9pk::merkle_archive::source::{
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
use crate::s9pk::v2::pack::{ImageSource, PackSource}; use crate::s9pk::v2::pack::{ImageSource, PackSource};
use crate::util::io::{open_file, TmpDir}; use crate::util::io::{open_file, TmpDir};
use crate::util::serde::IoFormat;
const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02];
@@ -33,6 +34,10 @@ pub mod pack;
├── icon.<ext> ├── icon.<ext>
├── LICENSE.md ├── LICENSE.md
├── instructions.md ├── instructions.md
├── dependencies
│ └── <id>
│ ├── metadata.json
│ └── icon.<ext>
├── javascript.squashfs ├── javascript.squashfs
├── assets ├── assets
│ └── <id>.squashfs (xN) │ └── <id>.squashfs (xN)
@@ -52,9 +57,10 @@ fn priority(s: &str) -> Option<usize> {
a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1), a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1),
"LICENSE.md" => Some(2), "LICENSE.md" => Some(2),
"instructions.md" => Some(3), "instructions.md" => Some(3),
"javascript.squashfs" => Some(4), "dependencies" => Some(4),
"assets" => Some(5), "javascript.squashfs" => Some(5),
"images" => Some(6), "assets" => Some(6),
"images" => Some(7),
_ => None, _ => None,
} }
} }
@@ -101,22 +107,16 @@ impl<S: FileSource + Clone> S9pk<S> {
filter.keep_checked(self.archive.contents_mut()) filter.keep_checked(self.archive.contents_mut())
} }
pub async fn icon(&self) -> Result<(InternedString, FileContents<S>), Error> { pub async fn icon(&self) -> Result<(InternedString, Entry<S>), Error> {
let mut best_icon = None; let mut best_icon = None;
for (path, icon) in self for (path, icon) in self.archive.contents().with_stem("icon").filter(|(p, v)| {
.archive Path::new(&*p)
.contents() .extension()
.with_stem("icon") .and_then(|e| e.to_str())
.filter(|(p, _)| { .and_then(mime)
Path::new(&*p) .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some())
.extension() }) {
.and_then(|e| e.to_str()) let size = icon.expect_file()?.size().await?;
.and_then(mime)
.map_or(false, |e| e.starts_with("image/"))
})
.filter_map(|(k, v)| v.into_file().map(|f| (k, f)))
{
let size = icon.size().await?;
best_icon = match best_icon { best_icon = match best_icon {
Some((s, a)) if s >= size => Some((s, a)), Some((s, a)) if s >= size => Some((s, a)),
_ => Some((size, (path, icon))), _ => Some((size, (path, icon))),
@@ -134,7 +134,75 @@ impl<S: FileSource + Clone> S9pk<S> {
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.and_then(mime) .and_then(mime)
.unwrap_or("image/png"); .unwrap_or("image/png");
DataUrl::from_reader(mime, contents.reader().await?, Some(contents.size().await?)).await Ok(DataUrl::from_vec(
mime,
contents.expect_file()?.to_vec(contents.hash()).await?,
))
}
pub async fn dependency_icon(
&self,
id: &PackageId,
) -> Result<Option<(InternedString, Entry<S>)>, Error> {
let mut best_icon = None;
for (path, icon) in self
.archive
.contents()
.get_path(Path::new("dependencies").join(id))
.and_then(|p| p.as_directory())
.into_iter()
.flat_map(|d| {
d.with_stem("icon").filter(|(p, v)| {
Path::new(&*p)
.extension()
.and_then(|e| e.to_str())
.and_then(mime)
.map_or(false, |e| e.starts_with("image/") && v.as_file().is_some())
})
})
{
let size = icon.expect_file()?.size().await?;
best_icon = match best_icon {
Some((s, a)) if s >= size => Some((s, a)),
_ => Some((size, (path, icon))),
};
}
Ok(best_icon.map(|(_, a)| a))
}
pub async fn dependency_icon_data_url(
&self,
id: &PackageId,
) -> Result<Option<DataUrl<'static>>, Error> {
let Some((name, contents)) = self.dependency_icon(id).await? else {
return Ok(None);
};
let mime = Path::new(&*name)
.extension()
.and_then(|e| e.to_str())
.and_then(mime)
.unwrap_or("image/png");
Ok(Some(DataUrl::from_vec(
mime,
contents.expect_file()?.to_vec(contents.hash()).await?,
)))
}
pub async fn dependency_metadata(
&self,
id: &PackageId,
) -> Result<Option<DependencyMetadata>, Error> {
if let Some(entry) = self
.archive
.contents()
.get_path(Path::new("dependencies").join(id).join("metadata.json"))
{
Ok(Some(IoFormat::Json.from_slice(
&entry.expect_file()?.to_vec(entry.hash()).await?,
)?))
} else {
Ok(None)
}
} }
pub async fn serialize<W: Sink>(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { pub async fn serialize<W: Sink>(&mut self, w: &mut W, verify: bool) -> Result<(), Error> {

View File

@@ -1,6 +1,4 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::io::Cursor;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@@ -10,25 +8,29 @@ use futures::{FutureExt, TryStreamExt};
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{ImageId, PackageId, VersionString}; use models::{ImageId, PackageId, VersionString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncRead;
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use tokio_stream::wrappers::ReadDirStream; use tokio_stream::wrappers::ReadDirStream;
use tracing::{debug, warn};
use ts_rs::TS; use ts_rs::TS;
use crate::context::CliContext; use crate::context::CliContext;
use crate::dependencies::DependencyMetadata;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::{ use crate::s9pk::merkle_archive::source::{
into_dyn_read, ArchiveSource, DynFileSource, FileSource, TmpSource, into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource,
}; };
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::io::{create_file, open_file, TmpDir};
use crate::util::Invoke; use crate::util::serde::IoFormat;
use crate::util::{new_guid, Invoke, PathOrUrl};
#[cfg(not(feature = "docker"))] #[cfg(not(feature = "docker"))]
pub const CONTAINER_TOOL: &str = "podman"; pub const CONTAINER_TOOL: &str = "podman";
@@ -83,7 +85,8 @@ pub enum PackSource {
Squashfs(Arc<SqfsDir>), Squashfs(Arc<SqfsDir>),
} }
impl FileSource for PackSource { impl FileSource for PackSource {
type Reader = Box<dyn AsyncRead + Unpin + Send + Sync + 'static>; type Reader = DynRead;
type SliceReader = DynRead;
async fn size(&self) -> Result<u64, Error> { async fn size(&self) -> Result<u64, Error> {
match self { match self {
Self::Buffered(a) => Ok(a.len() as u64), Self::Buffered(a) => Ok(a.len() as u64),
@@ -102,11 +105,23 @@ impl FileSource for PackSource {
} }
async fn reader(&self) -> Result<Self::Reader, Error> { async fn reader(&self) -> Result<Self::Reader, Error> {
match self { match self {
Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), Self::Buffered(a) => Ok(into_dyn_read(FileSource::reader(a).await?)),
Self::File(f) => Ok(into_dyn_read(open_file(f).await?)), Self::File(f) => Ok(into_dyn_read(FileSource::reader(f).await?)),
Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read),
} }
} }
async fn slice(&self, position: u64, size: u64) -> Result<Self::SliceReader, Error> {
match self {
Self::Buffered(a) => Ok(into_dyn_read(FileSource::slice(a, position, size).await?)),
Self::File(f) => Ok(into_dyn_read(FileSource::slice(f, position, size).await?)),
Self::Squashfs(dir) => dir
.file()
.await?
.fetch(position, size)
.await
.map(into_dyn_read),
}
}
} }
impl From<PackSource> for DynFileSource { impl From<PackSource> for DynFileSource {
fn from(value: PackSource) -> Self { fn from(value: PackSource) -> Self {
@@ -150,24 +165,71 @@ impl PackParams {
if let Some(icon) = &self.icon { if let Some(icon) = &self.icon {
Ok(icon.clone()) Ok(icon.clone())
} else { } else {
ReadDirStream::new(tokio::fs::read_dir(self.path()).await?).try_filter(|x| ready(x.path().file_stem() == Some(OsStr::new("icon")))).map_err(Error::from).try_fold(Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), |acc, x| async move { match acc { ReadDirStream::new(tokio::fs::read_dir(self.path()).await?)
Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), .try_filter(|x| {
Err(e) => Ok({ ready(
let path = x.path(); x.path()
if path.file_stem().and_then(|s| s.to_str()) == Some("icon") { .file_stem()
Ok(path) .map_or(false, |s| s.eq_ignore_ascii_case("icon")),
} else { )
Err(e)
}
}) })
}}).await? .map_err(Error::from)
.try_fold(
Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)),
|acc, x| async move {
match acc {
Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)),
Err(e) => Ok({
let path = x.path();
if path
.file_stem()
.map_or(false, |s| s.eq_ignore_ascii_case("icon"))
{
Ok(path)
} else {
Err(e)
}
}),
}
},
)
.await?
} }
} }
fn license(&self) -> PathBuf { async fn license(&self) -> Result<PathBuf, Error> {
self.license if let Some(license) = &self.license {
.as_ref() Ok(license.clone())
.cloned() } else {
.unwrap_or_else(|| self.path().join("LICENSE.md")) ReadDirStream::new(tokio::fs::read_dir(self.path()).await?)
.try_filter(|x| {
ready(
x.path()
.file_stem()
.map_or(false, |s| s.eq_ignore_ascii_case("license")),
)
})
.map_err(Error::from)
.try_fold(
Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)),
|acc, x| async move {
match acc {
Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)),
Err(e) => Ok({
let path = x.path();
if path
.file_stem()
.map_or(false, |s| s.eq_ignore_ascii_case("license"))
{
Ok(path)
} else {
Err(e)
}
}),
}
},
)
.await?
}
} }
fn instructions(&self) -> PathBuf { fn instructions(&self) -> PathBuf {
self.instructions self.instructions
@@ -282,6 +344,15 @@ pub enum ImageSource {
DockerTag(String), DockerTag(String),
} }
impl ImageSource { impl ImageSource {
pub fn ingredients(&self) -> Vec<PathBuf> {
match self {
Self::Packed => Vec::new(),
Self::DockerBuild { dockerfile, .. } => {
vec![dockerfile.clone().unwrap_or_else(|| "Dockerfile".into())]
}
Self::DockerTag(_) => Vec::new(),
}
}
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn load<'a, S: From<TmpSource<PackSource>> + FileSource + Clone>( pub fn load<'a, S: From<TmpSource<PackSource>> + FileSource + Clone>(
&'a self, &'a self,
@@ -320,7 +391,7 @@ impl ImageSource {
format!("--platform=linux/{arch}") format!("--platform=linux/{arch}")
}; };
// docker buildx build ${path} -o type=image,name=start9/${id} // docker buildx build ${path} -o type=image,name=start9/${id}
let tag = format!("start9/{id}/{image_id}:{version}"); let tag = format!("start9/{id}/{image_id}:{}", new_guid());
Command::new(CONTAINER_TOOL) Command::new(CONTAINER_TOOL)
.arg("build") .arg("build")
.arg(workdir) .arg(workdir)
@@ -501,7 +572,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
"LICENSE.md".into(), "LICENSE.md".into(),
Entry::file(TmpSource::new( Entry::file(TmpSource::new(
tmp_dir.clone(), tmp_dir.clone(),
PackSource::File(params.license()), PackSource::File(params.license().await?),
)), )),
); );
files.insert( files.insert(
@@ -541,6 +612,54 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
s9pk.load_images(tmp_dir.clone()).await?; s9pk.load_images(tmp_dir.clone()).await?;
let mut to_insert = Vec::new();
for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 {
if let Some(s9pk) = dependency.s9pk.take() {
let s9pk = match s9pk {
PathOrUrl::Path(path) => {
S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None)
.await?
.into_dyn()
}
PathOrUrl::Url(url) => {
if url.scheme() == "http" || url.scheme() == "https" {
S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url).await?),
None,
)
.await?
.into_dyn()
} else {
return Err(Error::new(
eyre!("unknown scheme: {}", url.scheme()),
ErrorKind::InvalidRequest,
));
}
}
};
let dep_path = Path::new("dependencies").join(id);
to_insert.push((
dep_path.join("metadata.json"),
Entry::file(PackSource::Buffered(
IoFormat::Json
.to_vec(&DependencyMetadata {
title: s9pk.as_manifest().title.clone(),
})?
.into(),
)),
));
let icon = s9pk.icon().await?;
to_insert.push((
dep_path.join(&*icon.0),
Entry::file(PackSource::Buffered(
icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(),
)),
));
} else {
warn!("no s9pk specified for {id}, leaving metadata empty");
}
}
s9pk.validate_and_filter(None)?; s9pk.validate_and_filter(None)?;
s9pk.serialize( s9pk.serialize(
@@ -555,3 +674,58 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[instrument(skip_all)]
pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result<Vec<PathBuf>, Error> {
let js_path = params.javascript().join("index.js");
let manifest: Manifest = match async {
serde_json::from_slice(
&Command::new("node")
.arg("-e")
.arg(format!(
"console.log(JSON.stringify(require('{}').manifest))",
js_path.display()
))
.invoke(ErrorKind::Javascript)
.await?,
)
.with_kind(ErrorKind::Deserialization)
}
.await
{
Ok(m) => m,
Err(e) => {
warn!("failed to load manifest: {e}");
debug!("{e:?}");
return Ok(vec![
js_path,
params.icon().await?,
params.license().await?,
params.instructions(),
]);
}
};
let mut ingredients = vec![
js_path,
params.icon().await?,
params.license().await?,
params.instructions(),
];
for (_, dependency) in manifest.dependencies.0 {
if let Some(PathOrUrl::Path(p)) = dependency.s9pk {
ingredients.push(p);
}
}
let assets_dir = params.assets();
for assets in manifest.assets {
ingredients.push(assets_dir.join(assets));
}
for image in manifest.images.values() {
ingredients.extend(image.source.ingredients());
}
Ok(ingredients)
}

View File

@@ -1147,18 +1147,14 @@ enum DependencyRequirement {
#[ts(type = "string[]")] #[ts(type = "string[]")]
health_checks: BTreeSet<HealthCheckId>, health_checks: BTreeSet<HealthCheckId>,
#[ts(type = "string")] #[ts(type = "string")]
version_spec: VersionRange, version_range: VersionRange,
#[ts(type = "string")]
registry_url: Url,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
Exists { Exists {
#[ts(type = "string")] #[ts(type = "string")]
id: PackageId, id: PackageId,
#[ts(type = "string")] #[ts(type = "string")]
version_spec: VersionRange, version_range: VersionRange,
#[ts(type = "string")]
registry_url: Url,
}, },
} }
// filebrowser:exists,bitcoind:running:foo+bar+baz // filebrowser:exists,bitcoind:running:foo+bar+baz
@@ -1168,8 +1164,7 @@ impl FromStr for DependencyRequirement {
match s.split_once(':') { match s.split_once(':') {
Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists {
id: id.parse()?, id: id.parse()?,
registry_url: "".parse()?, // TODO version_range: "*".parse()?, // TODO
version_spec: "*".parse()?, // TODO
}), }),
Some((id, rest)) => { Some((id, rest)) => {
let health_checks = match rest.split_once(':') { let health_checks = match rest.split_once(':') {
@@ -1192,15 +1187,13 @@ impl FromStr for DependencyRequirement {
Ok(Self::Running { Ok(Self::Running {
id: id.parse()?, id: id.parse()?,
health_checks, health_checks,
registry_url: "".parse()?, // TODO version_range: "*".parse()?, // TODO
version_spec: "*".parse()?, // TODO
}) })
} }
None => Ok(Self::Running { None => Ok(Self::Running {
id: s.parse()?, id: s.parse()?,
health_checks: BTreeSet::new(), health_checks: BTreeSet::new(),
registry_url: "".parse()?, // TODO version_range: "*".parse()?, // TODO
version_spec: "*".parse()?, // TODO
}), }),
} }
} }
@@ -1234,59 +1227,20 @@ async fn set_dependencies(
let mut deps = BTreeMap::new(); let mut deps = BTreeMap::new();
for dependency in dependencies { for dependency in dependencies {
let (dep_id, kind, registry_url, version_spec) = match dependency { let (dep_id, kind, version_range) = match dependency {
DependencyRequirement::Exists { DependencyRequirement::Exists { id, version_range } => {
id, (id, CurrentDependencyKind::Exists, version_range)
registry_url, }
version_spec,
} => (
id,
CurrentDependencyKind::Exists,
registry_url,
version_spec,
),
DependencyRequirement::Running { DependencyRequirement::Running {
id, id,
health_checks, health_checks,
registry_url, version_range,
version_spec,
} => ( } => (
id, id,
CurrentDependencyKind::Running { health_checks }, CurrentDependencyKind::Running { health_checks },
registry_url, version_range,
version_spec,
), ),
}; };
let (icon, title) = match async {
let remote_s9pk = S9pk::deserialize(
&Arc::new(
HttpSource::new(
context.seed.ctx.client.clone(),
registry_url
.join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?,
)
.await?,
),
None, // TODO
)
.await?;
let icon = remote_s9pk.icon_data_url().await?;
Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone()))
}
.await
{
Ok(a) => a,
Err(e) => {
tracing::error!("Error fetching remote s9pk: {e}");
tracing::debug!("{e:?}");
(
DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")),
dep_id.to_string(),
)
}
};
let config_satisfied = let config_satisfied =
if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await {
context context
@@ -1300,17 +1254,25 @@ async fn set_dependencies(
} else { } else {
true true
}; };
deps.insert( let info = CurrentDependencyInfo {
dep_id, title: context
CurrentDependencyInfo { .seed
kind, .persistent_container
registry_url, .s9pk
version_spec, .dependency_metadata(&dep_id)
icon, .await?
title, .map(|m| m.title),
config_satisfied, icon: context
}, .seed
); .persistent_container
.s9pk
.dependency_icon_data_url(&dep_id)
.await?,
kind,
version_range,
config_satisfied,
};
deps.insert(dep_id, info);
} }
context context
.seed .seed
@@ -1343,23 +1305,19 @@ async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRequir
.into_iter() .into_iter()
.map(|(id, current_dependency_info)| { .map(|(id, current_dependency_info)| {
let CurrentDependencyInfo { let CurrentDependencyInfo {
registry_url, version_range,
version_spec,
kind, kind,
.. ..
} = current_dependency_info; } = current_dependency_info;
Ok::<_, Error>(match kind { Ok::<_, Error>(match kind {
CurrentDependencyKind::Exists => DependencyRequirement::Exists { CurrentDependencyKind::Exists => {
id, DependencyRequirement::Exists { id, version_range }
registry_url, }
version_spec,
},
CurrentDependencyKind::Running { health_checks } => { CurrentDependencyKind::Running { health_checks } => {
DependencyRequirement::Running { DependencyRequirement::Running {
id, id,
health_checks, health_checks,
version_spec, version_range,
registry_url,
} }
} }
}) })
@@ -1381,7 +1339,8 @@ struct CheckDependenciesResult {
package_id: PackageId, package_id: PackageId,
is_installed: bool, is_installed: bool,
is_running: bool, is_running: bool,
health_checks: Vec<HealthCheckResult>, config_satisfied: bool,
health_checks: BTreeMap<HealthCheckId, HealthCheckResult>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
version: Option<exver::ExtendedVersion>, version: Option<exver::ExtendedVersion>,
} }
@@ -1415,24 +1374,27 @@ async fn check_dependencies(
package_id, package_id,
is_installed: false, is_installed: false,
is_running: false, is_running: false,
health_checks: vec![], config_satisfied: false,
health_checks: Default::default(),
version: None, version: None,
}); });
continue; continue;
}; };
let installed_version = package let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
.as_state_info() let installed_version = manifest.as_version().de()?.into_version();
.as_manifest(ManifestPreference::New) let satisfies = manifest.as_satisfies().de()?;
.as_version()
.de()?
.into_version();
let version = Some(installed_version.clone()); let version = Some(installed_version.clone());
if !installed_version.satisfies(&dependency_info.version_spec) { if ![installed_version]
.into_iter()
.chain(satisfies.into_iter().map(|v| v.into_version()))
.any(|v| v.satisfies(&dependency_info.version_range))
{
results.push(CheckDependenciesResult { results.push(CheckDependenciesResult {
package_id, package_id,
is_installed: false, is_installed: false,
is_running: false, is_running: false,
health_checks: vec![], config_satisfied: false,
health_checks: Default::default(),
version, version,
}); });
continue; continue;
@@ -1444,17 +1406,23 @@ async fn check_dependencies(
} else { } else {
false false
}; };
let health_checks = status let health_checks =
.health() if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind {
.cloned() status
.unwrap_or_default() .health()
.into_iter() .cloned()
.map(|(_, val)| val) .unwrap_or_default()
.collect(); .into_iter()
.filter(|(id, _)| health_checks.contains(id))
.collect()
} else {
Default::default()
};
results.push(CheckDependenciesResult { results.push(CheckDependenciesResult {
package_id, package_id,
is_installed, is_installed,
is_running, is_running,
config_satisfied: dependency_info.config_satisfied,
health_checks, health_checks,
version, version,
}); });

View File

@@ -1,13 +1,16 @@
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
use std::fmt;
use std::future::Future; use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
use std::process::Stdio; use std::process::Stdio;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration; use std::time::Duration;
use ::serde::{Deserialize, Serialize};
use async_trait::async_trait; use async_trait::async_trait;
use color_eyre::eyre::{self, eyre}; use color_eyre::eyre::{self, eyre};
use fd_lock_rs::FdLock; use fd_lock_rs::FdLock;
@@ -24,9 +27,12 @@ use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS;
use url::Url;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::io::create_file; use crate::util::io::create_file;
use crate::util::serde::{deserialize_from_str, serialize_display};
use crate::{Error, ErrorKind, ResultExt as _}; use crate::{Error, ErrorKind, ResultExt as _};
pub mod actor; pub mod actor;
pub mod clap; pub mod clap;
@@ -648,3 +654,48 @@ pub fn new_guid() -> InternedString {
&buf, &buf,
)) ))
} }
#[derive(Debug, Clone, TS)]
#[ts(type = "string")]
pub enum PathOrUrl {
Path(PathBuf),
Url(Url),
}
impl FromStr for PathOrUrl {
type Err = <PathBuf as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(url) = s.parse::<Url>() {
if url.scheme() == "file" {
Ok(Self::Path(url.path().parse()?))
} else {
Ok(Self::Url(url))
}
} else {
Ok(Self::Path(s.parse()?))
}
}
}
impl fmt::Display for PathOrUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path(p) => write!(f, "file://{}", p.display()),
Self::Url(u) => write!(f, "{u}"),
}
}
}
impl<'de> Deserialize<'de> for PathOrUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
deserialize_from_str(deserializer)
}
}
impl Serialize for PathOrUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
serialize_display(self, serializer)
}
}

View File

@@ -1,19 +1,25 @@
use core::fmt;
use std::borrow::Cow; use std::borrow::Cow;
use std::sync::Mutex;
use axum::extract::ws::{self, CloseFrame}; use axum::extract::ws::{self, CloseFrame};
use futures::Future; use futures::{Future, Stream, StreamExt};
use crate::prelude::*; use crate::prelude::*;
pub trait WebSocketExt { pub trait WebSocketExt {
fn normal_close( fn normal_close(
self, self,
msg: impl Into<Cow<'static, str>>, msg: impl Into<Cow<'static, str>> + Send,
) -> impl Future<Output = Result<(), Error>>; ) -> impl Future<Output = Result<(), Error>> + Send;
fn close_result(
self,
result: Result<impl Into<Cow<'static, str>> + Send, impl fmt::Display + Send>,
) -> impl Future<Output = Result<(), Error>> + Send;
} }
impl WebSocketExt for ws::WebSocket { impl WebSocketExt for ws::WebSocket {
async fn normal_close(mut self, msg: impl Into<Cow<'static, str>>) -> Result<(), Error> { async fn normal_close(mut self, msg: impl Into<Cow<'static, str>> + Send) -> Result<(), Error> {
self.send(ws::Message::Close(Some(CloseFrame { self.send(ws::Message::Close(Some(CloseFrame {
code: 1000, code: 1000,
reason: msg.into(), reason: msg.into(),
@@ -21,4 +27,41 @@ impl WebSocketExt for ws::WebSocket {
.await .await
.with_kind(ErrorKind::Network) .with_kind(ErrorKind::Network)
} }
async fn close_result(
mut self,
result: Result<impl Into<Cow<'static, str>> + Send, impl fmt::Display + Send>,
) -> Result<(), Error> {
match result {
Ok(msg) => self
.send(ws::Message::Close(Some(CloseFrame {
code: 1000,
reason: msg.into(),
})))
.await
.with_kind(ErrorKind::Network),
Err(e) => self
.send(ws::Message::Close(Some(CloseFrame {
code: 1011,
reason: e.to_string().into(),
})))
.await
.with_kind(ErrorKind::Network),
}
}
}
pub struct SyncBody(Mutex<axum::body::BodyDataStream>);
impl From<axum::body::Body> for SyncBody {
fn from(value: axum::body::Body) -> Self {
SyncBody(Mutex::new(value.into_data_stream()))
}
}
impl Stream for SyncBody {
type Item = <axum::body::BodyDataStream as Stream>::Item;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.0.lock().unwrap().poll_next_unpin(cx)
}
} }

View File

@@ -12,7 +12,7 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::util::io::{open_file, ParallelBlake3Writer}; use crate::util::io::{open_file, ParallelBlake3Writer};
use crate::util::serde::Base16; use crate::util::serde::Base16;
use crate::util::Apply; use crate::util::{Apply, PathOrUrl};
use crate::CAP_10_MiB; use crate::CAP_10_MiB;
pub fn util<C: Context>() -> ParentHandler<C> { pub fn util<C: Context>() -> ParentHandler<C> {
@@ -45,21 +45,20 @@ pub async fn b3sum(
} }
b3sum_source(file).await b3sum_source(file).await
} }
if let Ok(url) = file.parse::<Url>() { match file.parse::<PathOrUrl>()? {
if url.scheme() == "file" { PathOrUrl::Path(path) => b3sum_file(path, allow_mmap).await,
b3sum_file(url.path(), allow_mmap).await PathOrUrl::Url(url) => {
} else if url.scheme() == "http" || url.scheme() == "https" { if url.scheme() == "http" || url.scheme() == "https" {
HttpSource::new(ctx.client.clone(), url) HttpSource::new(ctx.client.clone(), url)
.await? .await?
.apply(b3sum_source) .apply(b3sum_source)
.await .await
} else { } else {
return Err(Error::new( Err(Error::new(
eyre!("unknown scheme: {}", url.scheme()), eyre!("unknown scheme: {}", url.scheme()),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ))
}
} }
} else {
b3sum_file(file, allow_mmap).await
} }
} }

View File

@@ -1,8 +1,10 @@
use std::any::Any;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use base64::Engine;
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::{ArgMatches, CommandFactory, FromArgMatches}; use clap::{ArgMatches, CommandFactory, FromArgMatches};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
@@ -37,7 +39,11 @@ pub fn deserialize_from_str<
{ {
type Value = T; type Value = T;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "a parsable string") write!(
formatter,
"a string that can be parsed as a {}",
std::any::type_name::<T>()
)
} }
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where where
@@ -988,18 +994,24 @@ impl<T: AsRef<[u8]>> Serialize for Base32<T> {
} }
} }
pub const BASE64: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
&base64::alphabet::STANDARD,
base64::engine::GeneralPurposeConfig::new(),
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)]
#[ts(type = "string", concrete(T = Vec<u8>))] #[ts(type = "string", concrete(T = Vec<u8>))]
pub struct Base64<T>(pub T); pub struct Base64<T>(pub T);
impl<T: AsRef<[u8]>> std::fmt::Display for Base64<T> { impl<T: AsRef<[u8]>> std::fmt::Display for Base64<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&base64::encode(self.0.as_ref())) f.write_str(&BASE64.encode(self.0.as_ref()))
} }
} }
impl<T: TryFrom<Vec<u8>>> FromStr for Base64<T> { impl<T: TryFrom<Vec<u8>>> FromStr for Base64<T> {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
base64::decode(&s) BASE64
.decode(&s)
.with_kind(ErrorKind::Deserialization)? .with_kind(ErrorKind::Deserialization)?
.apply(TryFrom::try_from) .apply(TryFrom::try_from)
.map(Self) .map(Self)

2
debian/postinst vendored
View File

@@ -49,9 +49,9 @@ managed=true
EOF EOF
$SYSTEMCTL enable startd.service $SYSTEMCTL enable startd.service
$SYSTEMCTL enable systemd-resolved.service $SYSTEMCTL enable systemd-resolved.service
$SYSTEMCTL enable systemd-networkd-wait-online.service
$SYSTEMCTL enable ssh.service $SYSTEMCTL enable ssh.service
$SYSTEMCTL disable wpa_supplicant.service $SYSTEMCTL disable wpa_supplicant.service
$SYSTEMCTL mask systemd-networkd-wait-online.service # currently use `NetworkManager-wait-online.service`
$SYSTEMCTL disable docker.service $SYSTEMCTL disable docker.service
$SYSTEMCTL disable postgresql.service $SYSTEMCTL disable postgresql.service

1
sdk/.prettierignore Normal file
View File

@@ -0,0 +1 @@
/lib/exver/exver.ts

View File

@@ -17,6 +17,9 @@ lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder
bundle: dist | test fmt bundle: dist | test fmt
touch dist touch dist
lib/exver/exver.ts: node_modules lib/exver/exver.pegjs
npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs
dist: $(TS_FILES) package.json node_modules README.md LICENSE dist: $(TS_FILES) package.json node_modules README.md LICENSE
npx tsc npx tsc
npx tsc --project tsconfig-cjs.json npx tsc --project tsconfig-cjs.json
@@ -31,7 +34,7 @@ check:
npm run check npm run check
fmt: node_modules fmt: node_modules
npx prettier --write "**/*.ts" npx prettier . "**/*.ts" --write
node_modules: package.json node_modules: package.json
npm ci npm ci

View File

@@ -5,4 +5,4 @@ module.exports = {
testEnvironment: "node", testEnvironment: "node",
rootDir: "./lib/", rootDir: "./lib/",
modulePathIgnorePatterns: ["./dist/"], modulePathIgnorePatterns: ["./dist/"],
}; }

View File

@@ -1,17 +1,17 @@
import { Checker } from "./emverLite/mod" import { VersionRange } from "./exver"
export class Dependency { export class Dependency {
constructor( constructor(
readonly data: readonly data:
| { | {
type: "running" type: "running"
versionSpec: Checker versionRange: VersionRange
registryUrl: string registryUrl: string
healthChecks: string[] healthChecks: string[]
} }
| { | {
type: "exists" type: "exists"
versionSpec: Checker versionRange: VersionRange
registryUrl: string registryUrl: string
}, },
) {} ) {}

View File

@@ -1,4 +1,3 @@
import { ManifestVersion, SDKManifest } from "./manifest/ManifestTypes"
import { RequiredDefault, Value } from "./config/builder/value" import { RequiredDefault, Value } from "./config/builder/value"
import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config" import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config"
import { import {
@@ -21,7 +20,6 @@ import {
MaybePromise, MaybePromise,
ServiceInterfaceId, ServiceInterfaceId,
PackageId, PackageId,
ValidIfNoStupidEscape,
} from "./types" } from "./types"
import * as patterns from "./util/patterns" import * as patterns from "./util/patterns"
import { DependencyConfig, Update } from "./dependencies/DependencyConfig" import { DependencyConfig, Update } from "./dependencies/DependencyConfig"
@@ -74,12 +72,14 @@ import { splitCommand } from "./util/splitCommand"
import { Mounts } from "./mainFn/Mounts" import { Mounts } from "./mainFn/Mounts"
import { Dependency } from "./Dependency" import { Dependency } from "./Dependency"
import * as T from "./types" import * as T from "./types"
import { Checker, EmVer } from "./emverLite/mod" import { testTypeVersion, ValidateExVer } from "./exver"
import { ExposedStorePaths } from "./store/setupExposeStore" import { ExposedStorePaths } from "./store/setupExposeStore"
import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder"
import { checkAllDependencies } from "./dependencies/dependencies" import { checkAllDependencies } from "./dependencies/dependencies"
import { health } from "." import { health } from "."
export const SDKVersion = testTypeVersion("0.3.6")
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =
T extends [] ? Else : T extends [] ? Else :
@@ -98,12 +98,12 @@ function removeConstType<E>() {
return <T>(t: T) => t as T & (E extends MainEffects ? {} : { const: never }) return <T>(t: T) => t as T & (E extends MainEffects ? {} : { const: never })
} }
export class StartSdk<Manifest extends SDKManifest, Store> { export class StartSdk<Manifest extends T.Manifest, Store> {
private constructor(readonly manifest: Manifest) {} private constructor(readonly manifest: Manifest) {}
static of() { static of() {
return new StartSdk<never, never>(null as never) return new StartSdk<never, never>(null as never)
} }
withManifest<Manifest extends SDKManifest = never>(manifest: Manifest) { withManifest<Manifest extends T.Manifest = never>(manifest: Manifest) {
return new StartSdk<Manifest, Store>(manifest) return new StartSdk<Manifest, Store>(manifest)
} }
withStore<Store extends Record<string, any>>() { withStore<Store extends Record<string, any>>() {
@@ -191,7 +191,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
id: keyof Manifest["images"] & T.ImageId id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean sharedRun?: boolean
}, },
command: ValidIfNoStupidEscape<A> | [string, ...string[]], command: T.CommandType,
options: CommandOptions & { options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[] mounts?: { path: string; options: MountOptions }[]
}, },
@@ -335,7 +335,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
([ ([
id, id,
{ {
data: { versionSpec, ...x }, data: { versionRange, ...x },
}, },
]) => ({ ]) => ({
id, id,
@@ -348,7 +348,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
: { : {
kind: "exists", kind: "exists",
}), }),
versionSpec: versionSpec.range, versionRange: versionRange.toString(),
}), }),
), ),
}) })
@@ -432,9 +432,6 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
spec: Spec, spec: Spec,
) => Config.of<Spec, Store>(spec), ) => Config.of<Spec, Store>(spec),
}, },
Checker: {
parse: Checker.parse,
},
Daemons: { Daemons: {
of(config: { of(config: {
effects: Effects effects: Effects
@@ -474,10 +471,6 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
>(dependencyConfig, update) >(dependencyConfig, update)
}, },
}, },
EmVer: {
from: EmVer.from,
parse: EmVer.parse,
},
List: { List: {
text: List.text, text: List.text,
obj: <Type extends Record<string, any>>( obj: <Type extends Record<string, any>>(
@@ -524,8 +517,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
) => List.dynamicText<Store>(getA), ) => List.dynamicText<Store>(getA),
}, },
Migration: { Migration: {
of: <Version extends ManifestVersion>(options: { of: <Version extends string>(options: {
version: Version version: Version & ValidateExVer<Version>
up: (opts: { effects: Effects }) => Promise<void> up: (opts: { effects: Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void> down: (opts: { effects: Effects }) => Promise<void>
}) => Migration.of<Manifest, Store, Version>(options), }) => Migration.of<Manifest, Store, Version>(options),
@@ -720,7 +713,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
} }
} }
export async function runCommand<Manifest extends SDKManifest>( export async function runCommand<Manifest extends T.Manifest>(
effects: Effects, effects: Effects,
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
command: string | [string, ...string[]], command: string | [string, ...string[]],

View File

@@ -1,12 +1,13 @@
import * as T from "../types"
import { Config, ExtractConfigType } from "../config/builder/config" import { Config, ExtractConfigType } from "../config/builder/config"
import { SDKManifest } from "../manifest/ManifestTypes"
import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types"
export type MaybeFn<Manifest extends SDKManifest, Store, Value> = export type MaybeFn<Manifest extends T.Manifest, Store, Value> =
| Value | Value
| ((options: { effects: Effects }) => Promise<Value> | Value) | ((options: { effects: Effects }) => Promise<Value> | Value)
export class CreatedAction< export class CreatedAction<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
@@ -30,7 +31,7 @@ export class CreatedAction<
) {} ) {}
static of< static of<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>

View File

@@ -1,8 +1,8 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { CreatedAction } from "./createAction" import { CreatedAction } from "./createAction"
export function setupActions<Manifest extends SDKManifest, Store>( export function setupActions<Manifest extends T.Manifest, Store>(
...createdActions: CreatedAction<Manifest, Store, any>[] ...createdActions: CreatedAction<Manifest, Store, any>[]
) { ) {
const myActions = async (options: { effects: Effects }) => { const myActions = async (options: { effects: Effects }) => {

View File

@@ -1,5 +1,3 @@
import { recursive } from "ts-matches"
import { SDKManifest } from "../manifest/ManifestTypes"
import * as T from "../types" import * as T from "../types"
import * as child_process from "child_process" import * as child_process from "child_process"
@@ -41,14 +39,14 @@ export type BackupSet<Volumes extends string> = {
* ).build()q * ).build()q
* ``` * ```
*/ */
export class Backups<M extends SDKManifest> { export class Backups<M extends T.Manifest> {
static BACKUP: BACKUP = "BACKUP" static BACKUP: BACKUP = "BACKUP"
private constructor( private constructor(
private options = DEFAULT_OPTIONS, private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet<M["volumes"][number]>[], private backupSet = [] as BackupSet<M["volumes"][number]>[],
) {} ) {}
static volumes<M extends SDKManifest = never>( static volumes<M extends T.Manifest = never>(
...volumeNames: Array<M["volumes"][0]> ...volumeNames: Array<M["volumes"][0]>
): Backups<M> { ): Backups<M> {
return new Backups<M>().addSets( return new Backups<M>().addSets(
@@ -60,12 +58,12 @@ export class Backups<M extends SDKManifest> {
})), })),
) )
} }
static addSets<M extends SDKManifest = never>( static addSets<M extends T.Manifest = never>(
...options: BackupSet<M["volumes"][0]>[] ...options: BackupSet<M["volumes"][0]>[]
) { ) {
return new Backups().addSets(...options) return new Backups().addSets(...options)
} }
static with_options<M extends SDKManifest = never>( static with_options<M extends T.Manifest = never>(
options?: Partial<T.BackupOptions>, options?: Partial<T.BackupOptions>,
) { ) {
return new Backups({ ...DEFAULT_OPTIONS, ...options }) return new Backups({ ...DEFAULT_OPTIONS, ...options })

View File

@@ -1,13 +1,13 @@
import { Backups } from "./Backups" import { Backups } from "./Backups"
import { SDKManifest } from "../manifest/ManifestTypes"
import { ExpectedExports, PathMaker } from "../types" import * as T from "../types"
import { _ } from "../util" import { _ } from "../util"
export type SetupBackupsParams<M extends SDKManifest> = Array< export type SetupBackupsParams<M extends T.Manifest> = Array<
M["volumes"][number] | Backups<M> M["volumes"][number] | Backups<M>
> >
export function setupBackups<M extends SDKManifest>( export function setupBackups<M extends T.Manifest>(
...args: _<SetupBackupsParams<M>> ...args: _<SetupBackupsParams<M>>
) { ) {
const backups = Array<Backups<M>>() const backups = Array<Backups<M>>()
@@ -21,22 +21,22 @@ export function setupBackups<M extends SDKManifest>(
} }
backups.push(Backups.volumes(...volumes)) backups.push(Backups.volumes(...volumes))
const answer: { const answer: {
createBackup: ExpectedExports.createBackup createBackup: T.ExpectedExports.createBackup
restoreBackup: ExpectedExports.restoreBackup restoreBackup: T.ExpectedExports.restoreBackup
} = { } = {
get createBackup() { get createBackup() {
return (async (options) => { return (async (options) => {
for (const backup of backups) { for (const backup of backups) {
await backup.build(options.pathMaker).createBackup(options) await backup.build(options.pathMaker).createBackup(options)
} }
}) as ExpectedExports.createBackup }) as T.ExpectedExports.createBackup
}, },
get restoreBackup() { get restoreBackup() {
return (async (options) => { return (async (options) => {
for (const backup of backups) { for (const backup of backups) {
await backup.build(options.pathMaker).restoreBackup(options) await backup.build(options.pathMaker).restoreBackup(options)
} }
}) as ExpectedExports.restoreBackup }) as T.ExpectedExports.restoreBackup
}, },
} }
return answer return answer

View File

@@ -1,22 +1,21 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Dependencies } from "../types"
export type ConfigDependencies<T extends SDKManifest> = { export type ConfigDependencies<T extends T.Manifest> = {
exists(id: keyof T["dependencies"]): Dependencies[number] exists(id: keyof T["dependencies"]): T.Dependencies[number]
running( running(
id: keyof T["dependencies"], id: keyof T["dependencies"],
healthChecks: string[], healthChecks: string[],
): Dependencies[number] ): T.Dependencies[number]
} }
export const configDependenciesSet = < export const configDependenciesSet = <
T extends SDKManifest, T extends T.Manifest,
>(): ConfigDependencies<T> => ({ >(): ConfigDependencies<T> => ({
exists(id: keyof T["dependencies"]) { exists(id: keyof T["dependencies"]) {
return { return {
id, id,
kind: "exists", kind: "exists",
} as Dependencies[number] } as T.Dependencies[number]
}, },
running(id: keyof T["dependencies"], healthChecks: string[]) { running(id: keyof T["dependencies"], healthChecks: string[]) {
@@ -24,6 +23,6 @@ export const configDependenciesSet = <
id, id,
kind: "running", kind: "running",
healthChecks, healthChecks,
} as Dependencies[number] } as T.Dependencies[number]
}, },
}) })

View File

@@ -1,5 +1,5 @@
import { Effects, ExpectedExports } from "../types" import * as T from "../types"
import { SDKManifest } from "../manifest/ManifestTypes"
import * as D from "./configDependencies" import * as D from "./configDependencies"
import { Config, ExtractConfigType } from "./builder/config" import { Config, ExtractConfigType } from "./builder/config"
import nullIfEmpty from "../util/nullIfEmpty" import nullIfEmpty from "../util/nullIfEmpty"
@@ -16,7 +16,7 @@ export type Save<
| Config<Record<string, any>, any> | Config<Record<string, any>, any>
| Config<Record<string, never>, never>, | Config<Record<string, never>, never>,
> = (options: { > = (options: {
effects: Effects effects: T.Effects
input: ExtractConfigType<A> & Record<string, any> input: ExtractConfigType<A> & Record<string, any>
}) => Promise<{ }) => Promise<{
dependenciesReceipt: DependenciesReceipt dependenciesReceipt: DependenciesReceipt
@@ -24,14 +24,14 @@ export type Save<
restart: boolean restart: boolean
}> }>
export type Read< export type Read<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
A extends A extends
| Record<string, any> | Record<string, any>
| Config<Record<string, any>, any> | Config<Record<string, any>, any>
| Config<Record<string, any>, never>, | Config<Record<string, any>, never>,
> = (options: { > = (options: {
effects: Effects effects: T.Effects
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)> }) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/** /**
* We want to setup a config export with a get and set, this * We want to setup a config export with a get and set, this
@@ -46,7 +46,7 @@ export function setupConfig<
| Record<string, any> | Record<string, any>
| Config<any, any> | Config<any, any>
| Config<any, never>, | Config<any, never>,
Manifest extends SDKManifest, Manifest extends T.Manifest,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
spec: Config<Type, Store> | Config<Type, never>, spec: Config<Type, Store> | Config<Type, never>,
@@ -69,7 +69,7 @@ export function setupConfig<
if (restart) { if (restart) {
await effects.restart() await effects.restart()
} }
}) as ExpectedExports.setConfig, }) as T.ExpectedExports.setConfig,
getConfig: (async ({ effects }) => { getConfig: (async ({ effects }) => {
const configValue = nullIfEmpty((await read({ effects })) || null) const configValue = nullIfEmpty((await read({ effects })) || null)
return { return {
@@ -78,7 +78,7 @@ export function setupConfig<
}), }),
config: configValue, config: configValue,
} }
}) as ExpectedExports.getConfig, }) as T.ExpectedExports.getConfig,
} }
} }

View File

@@ -1,11 +1,6 @@
import { import * as T from "../types"
DependencyConfig as DependencyConfigType,
DeepPartial,
Effects,
} from "../types"
import { deepEqual } from "../util/deepEqual" import { deepEqual } from "../util/deepEqual"
import { deepMerge } from "../util/deepMerge" import { deepMerge } from "../util/deepMerge"
import { SDKManifest } from "../manifest/ManifestTypes"
export type Update<QueryResults, RemoteConfig> = (options: { export type Update<QueryResults, RemoteConfig> = (options: {
remoteConfig: RemoteConfig remoteConfig: RemoteConfig
@@ -13,7 +8,7 @@ export type Update<QueryResults, RemoteConfig> = (options: {
}) => Promise<RemoteConfig> }) => Promise<RemoteConfig>
export class DependencyConfig< export class DependencyConfig<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
Input extends Record<string, any>, Input extends Record<string, any>,
RemoteConfig extends Record<string, any>, RemoteConfig extends Record<string, any>,
@@ -26,16 +21,16 @@ export class DependencyConfig<
} }
constructor( constructor(
readonly dependencyConfig: (options: { readonly dependencyConfig: (options: {
effects: Effects effects: T.Effects
localConfig: Input localConfig: Input
}) => Promise<void | DeepPartial<RemoteConfig>>, }) => Promise<void | T.DeepPartial<RemoteConfig>>,
readonly update: Update< readonly update: Update<
void | DeepPartial<RemoteConfig>, void | T.DeepPartial<RemoteConfig>,
RemoteConfig RemoteConfig
> = DependencyConfig.defaultUpdate as any, > = DependencyConfig.defaultUpdate as any,
) {} ) {}
async query(options: { effects: Effects; localConfig: unknown }) { async query(options: { effects: T.Effects; localConfig: unknown }) {
return this.dependencyConfig({ return this.dependencyConfig({
localConfig: options.localConfig as Input, localConfig: options.localConfig as Input,
effects: options.effects, effects: options.effects,

View File

@@ -3,20 +3,23 @@ import {
PackageId, PackageId,
DependencyRequirement, DependencyRequirement,
SetHealth, SetHealth,
CheckDependencyResult, CheckDependenciesResult,
} from "../types" } from "../types"
export type CheckAllDependencies = { export type CheckAllDependencies = {
notRunning: () => Promise<CheckDependencyResult[]> notInstalled: () => Promise<CheckDependenciesResult[]>
notRunning: () => Promise<CheckDependenciesResult[]>
notInstalled: () => Promise<CheckDependencyResult[]> configNotSatisfied: () => Promise<CheckDependenciesResult[]>
healthErrors: () => Promise<{ [id: string]: SetHealth[] }> healthErrors: () => Promise<{ [id: string]: SetHealth[] }>
throwIfNotRunning: () => Promise<void>
throwIfNotValid: () => Promise<undefined>
throwIfNotInstalled: () => Promise<void>
throwIfError: () => Promise<void>
isValid: () => Promise<boolean> isValid: () => Promise<boolean>
throwIfNotRunning: () => Promise<void>
throwIfNotInstalled: () => Promise<void>
throwIfConfigNotSatisfied: () => Promise<void>
throwIfHealthError: () => Promise<void>
throwIfNotValid: () => Promise<void>
} }
export function checkAllDependencies(effects: Effects): CheckAllDependencies { export function checkAllDependencies(effects: Effects): CheckAllDependencies {
const dependenciesPromise = effects.getDependencies() const dependenciesPromise = effects.getDependencies()
@@ -45,14 +48,16 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies {
if (!dependency) continue if (!dependency) continue
if (dependency.kind !== "running") continue if (dependency.kind !== "running") continue
const healthChecks = result.healthChecks const healthChecks = Object.entries(result.healthChecks)
.filter((x) => dependency.healthChecks.includes(x.id)) .map(([id, hc]) => ({ ...hc, id }))
.filter((x) => !!x.message) .filter((x) => !!x.message)
if (healthChecks.length === 0) continue if (healthChecks.length === 0) continue
answer[result.packageId] = healthChecks answer[result.packageId] = healthChecks
} }
return answer return answer
} }
const configNotSatisfied = () =>
resultsPromise.then((x) => x.filter((x) => !x.configSatisfied))
const notInstalled = () => const notInstalled = () =>
resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) resultsPromise.then((x) => x.filter((x) => !x.isInstalled))
const notRunning = async () => { const notRunning = async () => {
@@ -68,7 +73,7 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies {
const entries = <B>(x: { [k: string]: B }) => Object.entries(x) const entries = <B>(x: { [k: string]: B }) => Object.entries(x)
const first = <A>(x: A[]): A | undefined => x[0] const first = <A>(x: A[]): A | undefined => x[0]
const sinkVoid = <A>(x: A) => void 0 const sinkVoid = <A>(x: A) => void 0
const throwIfError = () => const throwIfHealthError = () =>
healthErrors() healthErrors()
.then(entries) .then(entries)
.then(first) .then(first)
@@ -78,6 +83,14 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies {
if (healthChecks.length > 0) if (healthChecks.length > 0)
throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}`
}) })
const throwIfConfigNotSatisfied = () =>
configNotSatisfied().then((results) => {
throw new Error(
`Package ${results[0].packageId} does not have a valid configuration`,
)
})
const throwIfNotRunning = () => const throwIfNotRunning = () =>
notRunning().then((results) => { notRunning().then((results) => {
if (results[0]) if (results[0])
@@ -93,7 +106,8 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies {
Promise.all([ Promise.all([
throwIfNotRunning(), throwIfNotRunning(),
throwIfNotInstalled(), throwIfNotInstalled(),
throwIfError(), throwIfConfigNotSatisfied(),
throwIfHealthError(),
]).then(sinkVoid) ]).then(sinkVoid)
const isValid = () => const isValid = () =>
@@ -105,11 +119,13 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies {
return { return {
notRunning, notRunning,
notInstalled, notInstalled,
configNotSatisfied,
healthErrors, healthErrors,
throwIfNotRunning, throwIfNotRunning,
throwIfConfigNotSatisfied,
throwIfNotValid, throwIfNotValid,
throwIfNotInstalled, throwIfNotInstalled,
throwIfError, throwIfHealthError,
isValid, isValid,
} }
} }

View File

@@ -1,12 +1,12 @@
import { Config } from "../config/builder/config" import { Config } from "../config/builder/config"
import { SDKManifest } from "../manifest/ManifestTypes"
import { ExpectedExports } from "../types" import * as T from "../types"
import { DependencyConfig } from "./DependencyConfig" import { DependencyConfig } from "./DependencyConfig"
export function setupDependencyConfig< export function setupDependencyConfig<
Store, Store,
Input extends Record<string, any>, Input extends Record<string, any>,
Manifest extends SDKManifest, Manifest extends T.Manifest,
>( >(
_config: Config<Input, Store> | Config<Input, never>, _config: Config<Input, Store> | Config<Input, never>,
autoConfigs: { autoConfigs: {
@@ -17,6 +17,6 @@ export function setupDependencyConfig<
any any
> | null > | null
}, },
): ExpectedExports.dependencyConfig { ): T.ExpectedExports.dependencyConfig {
return autoConfigs return autoConfigs
} }

View File

@@ -1,323 +0,0 @@
import * as matches from "ts-matches"
const starSub = /((\d+\.)*\d+)\.\*/
// prettier-ignore
export type ValidEmVer = string;
// prettier-ignore
export type ValidEmVerRange = string;
function incrementLastNumber(list: number[]) {
const newList = [...list]
newList[newList.length - 1]++
return newList
}
/**
* Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*`
* and return a checker, that has the check function for checking that a version is in the valid
* @param range
* @returns
*/
export function rangeOf(range: string | Checker): Checker {
return Checker.parse(range)
}
/**
* Used to create a checker that will `and` all the ranges passed in
* @param ranges
* @returns
*/
export function rangeAnd(...ranges: (string | Checker)[]): Checker {
if (ranges.length === 0) {
throw new Error("No ranges given")
}
const [firstCheck, ...rest] = ranges
return Checker.parse(firstCheck).and(...rest)
}
/**
* Used to create a checker that will `or` all the ranges passed in
* @param ranges
* @returns
*/
export function rangeOr(...ranges: (string | Checker)[]): Checker {
if (ranges.length === 0) {
throw new Error("No ranges given")
}
const [firstCheck, ...rest] = ranges
return Checker.parse(firstCheck).or(...rest)
}
/**
* This will negate the checker, so given a checker that checks for >= 1.0.0, it will check for < 1.0.0
* @param range
* @returns
*/
export function notRange(range: string | Checker): Checker {
return rangeOf(range).not()
}
/**
* EmVer is a set of versioning of any pattern like 1 or 1.2 or 1.2.3 or 1.2.3.4 or ..
*/
export class EmVer {
/**
* Convert the range, should be 1.2.* or * into a emver
* Or an already made emver
* IsUnsafe
*/
static from(range: string | EmVer): EmVer {
if (range instanceof EmVer) {
return range
}
return EmVer.parse(range)
}
/**
* Convert the range, should be 1.2.* or * into a emver
* IsUnsafe
*/
static parse(rangeExtra: string): EmVer {
const [range, extra] = rangeExtra.split("-")
const values = range.split(".").map((x) => parseInt(x))
for (const value of values) {
if (isNaN(value)) {
throw new Error(`Couldn't parse range: ${range}`)
}
}
return new EmVer(values, extra)
}
private constructor(
public readonly values: number[],
readonly extra: string | null,
) {}
/**
* Used when we need a new emver that has the last number incremented, used in the 1.* like things
*/
public withLastIncremented() {
return new EmVer(incrementLastNumber(this.values), null)
}
public greaterThan(other: EmVer): boolean {
for (const i in this.values) {
if (other.values[i] == null) {
return true
}
if (this.values[i] > other.values[i]) {
return true
}
if (this.values[i] < other.values[i]) {
return false
}
}
return false
}
public equals(other: EmVer): boolean {
if (other.values.length !== this.values.length) {
return false
}
for (const i in this.values) {
if (this.values[i] !== other.values[i]) {
return false
}
}
return true
}
public greaterThanOrEqual(other: EmVer): boolean {
return this.greaterThan(other) || this.equals(other)
}
public lessThanOrEqual(other: EmVer): boolean {
return !this.greaterThan(other)
}
public lessThan(other: EmVer): boolean {
return !this.greaterThanOrEqual(other)
}
/**
* Return a enum string that describes (used for switching/iffs)
* to know comparison
* @param other
* @returns
*/
public compare(other: EmVer) {
if (this.equals(other)) {
return "equal" as const
} else if (this.greaterThan(other)) {
return "greater" as const
} else {
return "less" as const
}
}
/**
* Used when sorting emver's in a list using the sort method
* @param other
* @returns
*/
public compareForSort(other: EmVer) {
return matches
.matches(this.compare(other))
.when("equal", () => 0 as const)
.when("greater", () => 1 as const)
.when("less", () => -1 as const)
.unwrap()
}
toString() {
return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}` as ValidEmVer
}
}
/**
* A checker is a function that takes a version and returns true if the version matches the checker.
* Used when we are doing range checking, like saying ">=1.0.0".check("1.2.3") will be true
*/
export class Checker {
/**
* Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*`
* and return a checker, that has the check function for checking that a version is in the valid
* @param range
* @returns
*/
static parse(range: string | Checker): Checker {
if (range instanceof Checker) {
return range
}
range = range.trim()
if (range.indexOf("||") !== -1) {
return rangeOr(...range.split("||").map((x) => Checker.parse(x)))
}
if (range.indexOf("&&") !== -1) {
return rangeAnd(...range.split("&&").map((x) => Checker.parse(x)))
}
if (range === "*") {
return new Checker((version) => {
EmVer.from(version)
return true
}, range)
}
if (range.startsWith("!!")) return Checker.parse(range.substring(2))
if (range.startsWith("!")) {
const tempValue = Checker.parse(range.substring(1))
return new Checker((x) => !tempValue.check(x), range)
}
const starSubMatches = starSub.exec(range)
if (starSubMatches != null) {
const emVarLower = EmVer.parse(starSubMatches[1])
const emVarUpper = emVarLower.withLastIncremented()
return new Checker((version) => {
const v = EmVer.from(version)
return (
(v.greaterThan(emVarLower) || v.equals(emVarLower)) &&
!v.greaterThan(emVarUpper) &&
!v.equals(emVarUpper)
)
}, range)
}
switch (range.substring(0, 2)) {
case ">=": {
const emVar = EmVer.parse(range.substring(2))
return new Checker((version) => {
const v = EmVer.from(version)
return v.greaterThanOrEqual(emVar)
}, range)
}
case "<=": {
const emVar = EmVer.parse(range.substring(2))
return new Checker((version) => {
const v = EmVer.from(version)
return v.lessThanOrEqual(emVar)
}, range)
}
}
switch (range.substring(0, 1)) {
case ">": {
const emVar = EmVer.parse(range.substring(1))
return new Checker((version) => {
const v = EmVer.from(version)
return v.greaterThan(emVar)
}, range)
}
case "<": {
const emVar = EmVer.parse(range.substring(1))
return new Checker((version) => {
const v = EmVer.from(version)
return v.lessThan(emVar)
}, range)
}
case "=": {
const emVar = EmVer.parse(range.substring(1))
return new Checker((version) => {
const v = EmVer.from(version)
return v.equals(emVar)
}, `=${emVar.toString()}`)
}
}
throw new Error("Couldn't parse range: " + range)
}
constructor(
/**
* Check is the function that will be given a emver or unparsed emver and should give if it follows
* a pattern
*/
public readonly check: (value: ValidEmVer | EmVer) => boolean,
private readonly _range: string,
) {}
get range() {
return this._range as ValidEmVerRange
}
/**
* Used when we want the `and` condition with another checker
*/
public and(...others: (Checker | string)[]): Checker {
const othersCheck = others.map(Checker.parse)
return new Checker(
(value) => {
if (!this.check(value)) {
return false
}
for (const other of othersCheck) {
if (!other.check(value)) {
return false
}
}
return true
},
othersCheck.map((x) => x._range).join(" && "),
)
}
/**
* Used when we want the `or` condition with another checker
*/
public or(...others: (Checker | string)[]): Checker {
const othersCheck = others.map(Checker.parse)
return new Checker(
(value) => {
if (this.check(value)) {
return true
}
for (const other of othersCheck) {
if (other.check(value)) {
return true
}
}
return false
},
othersCheck.map((x) => x._range).join(" || "),
)
}
/**
* A useful example is making sure we don't match an exact version, like !=1.2.3
* @returns
*/
public not(): Checker {
let newRange = `!${this._range}`
return Checker.parse(newRange)
}
}

99
sdk/lib/exver/exver.pegjs Normal file
View File

@@ -0,0 +1,99 @@
// #flavor:0.1.2-beta.1:0
// !( >=1:1 && <= 2:2)
VersionRange
= first:VersionRangeAtom rest:(_ ((Or / And) _)? VersionRangeAtom)*
Or = "||"
And = "&&"
VersionRangeAtom
= Parens
/ Anchor
/ Not
/ Any
/ None
Parens
= "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } }
Anchor
= operator:CmpOp? _ version:VersionSpec { return { type: "Anchor", operator, version } }
VersionSpec
= flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } }
Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }}
Any = "*" { return { type: "Any" } }
None = "!" { return { type: "None" } }
CmpOp
= ">=" { return ">="; }
/ "<=" { return "<="; }
/ ">" { return ">"; }
/ "<" { return "<"; }
/ "=" { return "="; }
/ "!=" { return "!="; }
/ "^" { return "^"; }
/ "~" { return "~"; }
ExtendedVersion
= flavor:Flavor? upstream:Version ":" downstream:Version {
return { flavor: flavor || null, upstream, downstream }
}
EmVer
= major:Digit "." minor:Digit "." patch:Digit ("." revision:Digit)? {
return {
flavor: null,
upstream: {
number: [major, minor, patch],
prerelease: [],
},
downstream: {
number: [revision || 0],
prerelease: [],
},
}
}
Flavor
= "#" flavor:Lowercase ":" { return flavor }
Lowercase
= [a-z]+ { return text() }
String
= [a-zA-Z]+ { return text(); }
Version
= number:VersionNumber prerelease: PreRelease? {
return {
number,
prerelease: prerelease || []
};
}
PreRelease
= "-" first:PreReleaseSegment rest:("." PreReleaseSegment)* {
return [first].concat(rest.map(r => r[1]));
}
PreReleaseSegment
= "."? segment:(Digit / String) {
return segment;
}
VersionNumber
= first:Digit rest:("." Digit)* {
return [first].concat(rest.map(r => r[1]));
}
Digit
= [0-9]+ { return parseInt(text(), 10); }
_ "whitespace"
= [ \t\n\r]*

2507
sdk/lib/exver/exver.ts Normal file

File diff suppressed because it is too large Load Diff

443
sdk/lib/exver/index.ts Normal file
View File

@@ -0,0 +1,443 @@
import * as P from "./exver"
// prettier-ignore
export type ValidateVersion<T extends String> =
T extends `-${infer A}` ? never :
T extends `${infer A}-${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
T extends `${bigint}` ? unknown :
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
never
// prettier-ignore
export type ValidateExVer<T extends string> =
T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
T extends `${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
never
// prettier-ignore
export type ValidateExVers<T> =
T extends [] ? unknown :
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
never
type Anchor = {
type: "Anchor"
operator: P.CmpOp
version: ExtendedVersion
}
type And = {
type: "And"
left: VersionRange
right: VersionRange
}
type Or = {
type: "Or"
left: VersionRange
right: VersionRange
}
type Not = {
type: "Not"
value: VersionRange
}
export class VersionRange {
private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {}
toString(): string {
switch (this.atom.type) {
case "Anchor":
return `${this.atom.operator}${this.atom.version}`
case "And":
return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})`
case "Or":
return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})`
case "Not":
return `!(${this.atom.value.toString()})`
case "Any":
return "*"
case "None":
return "!"
}
}
/**
* Returns a boolean indicating whether a given version satisfies the VersionRange
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
*/
satisfiedBy(version: ExtendedVersion): boolean {
switch (this.atom.type) {
case "Anchor":
const otherVersion = this.atom.version
switch (this.atom.operator) {
case "=":
return version.equals(otherVersion)
case ">":
return version.greaterThan(otherVersion)
case "<":
return version.lessThan(otherVersion)
case ">=":
return version.greaterThanOrEqual(otherVersion)
case "<=":
return version.lessThanOrEqual(otherVersion)
case "!=":
return !version.equals(otherVersion)
case "^":
const nextMajor = this.atom.version.incrementMajor()
if (
version.greaterThanOrEqual(otherVersion) &&
version.lessThan(nextMajor)
) {
return true
} else {
return false
}
case "~":
const nextMinor = this.atom.version.incrementMinor()
if (
version.greaterThanOrEqual(otherVersion) &&
version.lessThan(nextMinor)
) {
return true
} else {
return false
}
}
case "And":
return (
this.atom.left.satisfiedBy(version) &&
this.atom.right.satisfiedBy(version)
)
case "Or":
return (
this.atom.left.satisfiedBy(version) ||
this.atom.right.satisfiedBy(version)
)
case "Not":
return !this.atom.value.satisfiedBy(version)
case "Any":
return true
case "None":
return false
}
}
private static parseAtom(atom: P.VersionRangeAtom): VersionRange {
switch (atom.type) {
case "Not":
return new VersionRange({
type: "Not",
value: VersionRange.parseAtom(atom.value),
})
case "Parens":
return VersionRange.parseRange(atom.expr)
case "Anchor":
return new VersionRange({
type: "Anchor",
operator: atom.operator || "^",
version: new ExtendedVersion(
atom.version.flavor,
new Version(
atom.version.upstream.number,
atom.version.upstream.prerelease,
),
new Version(
atom.version.downstream.number,
atom.version.downstream.prerelease,
),
),
})
default:
return new VersionRange(atom)
}
}
private static parseRange(range: P.VersionRange): VersionRange {
let result = VersionRange.parseAtom(range[0])
for (const next of range[1]) {
switch (next[1]?.[0]) {
case "||":
result = new VersionRange({
type: "Or",
left: result,
right: VersionRange.parseAtom(next[2]),
})
break
case "&&":
default:
result = new VersionRange({
type: "And",
left: result,
right: VersionRange.parseAtom(next[2]),
})
break
}
}
return result
}
static parse(range: string): VersionRange {
return VersionRange.parseRange(
P.parse(range, { startRule: "VersionRange" }),
)
}
and(right: VersionRange) {
return new VersionRange({ type: "And", left: this, right })
}
or(right: VersionRange) {
return new VersionRange({ type: "Or", left: this, right })
}
not() {
return new VersionRange({ type: "Not", value: this })
}
static anchor(operator: P.CmpOp, version: ExtendedVersion) {
return new VersionRange({ type: "Anchor", operator, version })
}
static any() {
return new VersionRange({ type: "Any" })
}
static none() {
return new VersionRange({ type: "None" })
}
}
export class Version {
constructor(
public number: number[],
public prerelease: (string | number)[],
) {}
toString(): string {
return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}`
}
compare(other: Version): "greater" | "equal" | "less" {
const numLen = Math.max(this.number.length, other.number.length)
for (let i = 0; i < numLen; i++) {
if ((this.number[i] || 0) > (other.number[i] || 0)) {
return "greater"
} else if ((this.number[i] || 0) < (other.number[i] || 0)) {
return "less"
}
}
if (this.prerelease.length === 0 && other.prerelease.length !== 0) {
return "greater"
} else if (this.prerelease.length !== 0 && other.prerelease.length === 0) {
return "less"
}
const prereleaseLen = Math.max(this.number.length, other.number.length)
for (let i = 0; i < prereleaseLen; i++) {
if (typeof this.prerelease[i] === typeof other.prerelease[i]) {
if (this.prerelease[i] > other.prerelease[i]) {
return "greater"
} else if (this.prerelease[i] < other.prerelease[i]) {
return "less"
}
} else {
switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) {
case "number:string":
return "less"
case "string:number":
return "greater"
case "number:undefined":
case "string:undefined":
return "greater"
case "undefined:number":
case "undefined:string":
return "less"
}
}
}
return "equal"
}
static parse(version: string): Version {
const parsed = P.parse(version, { startRule: "Version" })
return new Version(parsed.number, parsed.prerelease)
}
}
// #flavor:0.1.2-beta.1:0
export class ExtendedVersion {
constructor(
public flavor: string | null,
public upstream: Version,
public downstream: Version,
) {}
toString(): string {
return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}`
}
compare(other: ExtendedVersion): "greater" | "equal" | "less" | null {
if (this.flavor !== other.flavor) {
return null
}
const upstreamCmp = this.upstream.compare(other.upstream)
if (upstreamCmp !== "equal") {
return upstreamCmp
}
return this.downstream.compare(other.downstream)
}
compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" {
if ((this.flavor || "") > (other.flavor || "")) {
return "greater"
} else if ((this.flavor || "") > (other.flavor || "")) {
return "less"
} else {
return this.compare(other)!
}
}
compareForSort(other: ExtendedVersion): 1 | 0 | -1 {
switch (this.compareLexicographic(other)) {
case "greater":
return 1
case "equal":
return 0
case "less":
return -1
}
}
greaterThan(other: ExtendedVersion): boolean {
return this.compare(other) === "greater"
}
greaterThanOrEqual(other: ExtendedVersion): boolean {
return ["greater", "equal"].includes(this.compare(other) as string)
}
equals(other: ExtendedVersion): boolean {
return this.compare(other) === "equal"
}
lessThan(other: ExtendedVersion): boolean {
return this.compare(other) === "less"
}
lessThanOrEqual(other: ExtendedVersion): boolean {
return ["less", "equal"].includes(this.compare(other) as string)
}
static parse(extendedVersion: string): ExtendedVersion {
const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" })
return new ExtendedVersion(
parsed.flavor,
new Version(parsed.upstream.number, parsed.upstream.prerelease),
new Version(parsed.downstream.number, parsed.downstream.prerelease),
)
}
static parseEmver(extendedVersion: string): ExtendedVersion {
const parsed = P.parse(extendedVersion, { startRule: "EmVer" })
return new ExtendedVersion(
parsed.flavor,
new Version(parsed.upstream.number, parsed.upstream.prerelease),
new Version(parsed.downstream.number, parsed.downstream.prerelease),
)
}
/**
* Returns an ExtendedVersion with the Upstream major version version incremented by 1
* and sets subsequent digits to zero.
* If no non-zero upstream digit can be found the last upstream digit will be incremented.
*/
incrementMajor(): ExtendedVersion {
const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0)
const majorNumber = this.upstream.number.map((num, idx): number => {
if (idx > majorIdx) {
return 0
} else if (idx === majorIdx) {
return num + 1
}
return num
})
const incrementedUpstream = new Version(majorNumber, [])
const updatedDownstream = new Version([0], [])
return new ExtendedVersion(
this.flavor,
incrementedUpstream,
updatedDownstream,
)
}
/**
* Returns an ExtendedVersion with the Upstream minor version version incremented by 1
* also sets subsequent digits to zero.
* If no non-zero upstream digit can be found the last digit will be incremented.
*/
incrementMinor(): ExtendedVersion {
const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0)
let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1
const majorNumber = this.upstream.number.map((num, idx): number => {
if (idx > minorIdx) {
return 0
} else if (idx === minorIdx) {
return num + 1
}
return num
})
const incrementedUpstream = new Version(majorNumber, [])
const updatedDownstream = new Version([0], [])
return new ExtendedVersion(
this.flavor,
incrementedUpstream,
updatedDownstream,
)
}
}
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
export const testTypeVersion = <T extends string>(t: T & ValidateVersion<T>) =>
t
function tests() {
testTypeVersion("1.2.3")
testTypeVersion("1")
testTypeVersion("12.34.56")
testTypeVersion("1.2-3")
testTypeVersion("1-3")
// @ts-expect-error
testTypeVersion("-3")
// @ts-expect-error
testTypeVersion("1.2.3:1")
// @ts-expect-error
testTypeVersion("#cat:1:1")
testTypeExVer("1.2.3:1.2.3")
testTypeExVer("1.2.3.4.5.6.7.8.9.0:1")
testTypeExVer("100:1")
testTypeExVer("#cat:1:1")
testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1")
testTypeExVer("1-0:1")
testTypeExVer("1-0:1")
// @ts-expect-error
testTypeExVer("1.2-3")
// @ts-expect-error
testTypeExVer("1-3")
// @ts-expect-error
testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string)
// @ts-expect-error
testTypeExVer("1.-2:1")
// @ts-expect-error
testTypeExVer("1..2.3:3")
}

View File

@@ -1,5 +1,4 @@
import { InterfaceReceipt } from "../interfaces/interfaceReceipt" import { Effects } from "../types"
import { Daemon, Effects, SDKManifest } from "../types"
import { CheckResult } from "./checkFns/CheckResult" import { CheckResult } from "./checkFns/CheckResult"
import { HealthReceipt } from "./HealthReceipt" import { HealthReceipt } from "./HealthReceipt"
import { Trigger } from "../trigger" import { Trigger } from "../trigger"
@@ -8,9 +7,9 @@ import { defaultTrigger } from "../trigger/defaultTrigger"
import { once } from "../util/once" import { once } from "../util/once"
import { Overlay } from "../util/Overlay" import { Overlay } from "../util/Overlay"
import { object, unknown } from "ts-matches" import { object, unknown } from "ts-matches"
import { T } from ".." import * as T from "../types"
export type HealthCheckParams<Manifest extends SDKManifest> = { export type HealthCheckParams<Manifest extends T.Manifest> = {
effects: Effects effects: Effects
name: string name: string
image: { image: {
@@ -22,7 +21,7 @@ export type HealthCheckParams<Manifest extends SDKManifest> = {
onFirstSuccess?: () => unknown | Promise<unknown> onFirstSuccess?: () => unknown | Promise<unknown>
} }
export function healthCheck<Manifest extends SDKManifest>( export function healthCheck<Manifest extends T.Manifest>(
o: HealthCheckParams<Manifest>, o: HealthCheckParams<Manifest>,
) { ) {
new Promise(async () => { new Promise(async () => {

View File

@@ -1,12 +1,10 @@
export { EmVer } from "./emverLite/mod"
export { setupManifest } from "./manifest/setupManifest"
export { setupExposeStore } from "./store/setupExposeStore"
export { S9pk } from "./s9pk" export { S9pk } from "./s9pk"
export { VersionRange, ExtendedVersion, Version } from "./exver"
export * as config from "./config" export * as config from "./config"
export * as CB from "./config/builder" export * as CB from "./config/builder"
export * as CT from "./config/configTypes" export * as CT from "./config/configTypes"
export * as dependencyConfig from "./dependencies" export * as dependencyConfig from "./dependencies"
export * as manifest from "./manifest"
export * as types from "./types" export * as types from "./types"
export * as T from "./types" export * as T from "./types"
export * as yaml from "yaml" export * as yaml from "yaml"

View File

@@ -1,5 +1,4 @@
export { Daemons } from "./mainFn/Daemons" export { Daemons } from "./mainFn/Daemons"
export { EmVer } from "./emverLite/mod"
export { Overlay } from "./util/Overlay" export { Overlay } from "./util/Overlay"
export { StartSdk } from "./StartSdk" export { StartSdk } from "./StartSdk"
export { setupManifest } from "./manifest/setupManifest" export { setupManifest } from "./manifest/setupManifest"
@@ -7,6 +6,7 @@ export { FileHelper } from "./util/fileHelper"
export { setupExposeStore } from "./store/setupExposeStore" export { setupExposeStore } from "./store/setupExposeStore"
export { pathBuilder } from "./store/PathBuilder" export { pathBuilder } from "./store/PathBuilder"
export { S9pk } from "./s9pk" export { S9pk } from "./s9pk"
export { VersionRange, ExtendedVersion, Version } from "./exver"
export * as actions from "./actions" export * as actions from "./actions"
export * as backup from "./backup" export * as backup from "./backup"

View File

@@ -1,35 +1,35 @@
import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" import { ValidateExVer } from "../../exver"
import { Effects } from "../../types" import * as T from "../../types"
export class Migration< export class Migration<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
Version extends ManifestVersion, Version extends string,
> { > {
constructor( constructor(
readonly options: { readonly options: {
version: Version version: Version & ValidateExVer<Version>
up: (opts: { effects: Effects }) => Promise<void> up: (opts: { effects: T.Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void> down: (opts: { effects: T.Effects }) => Promise<void>
}, },
) {} ) {}
static of< static of<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
Version extends ManifestVersion, Version extends string,
>(options: { >(options: {
version: Version version: Version & ValidateExVer<Version>
up: (opts: { effects: Effects }) => Promise<void> up: (opts: { effects: T.Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void> down: (opts: { effects: T.Effects }) => Promise<void>
}) { }) {
return new Migration<Manifest, Store, Version>(options) return new Migration<Manifest, Store, Version>(options)
} }
async up(opts: { effects: Effects }) { async up(opts: { effects: T.Effects }) {
this.up(opts) this.up(opts)
} }
async down(opts: { effects: Effects }) { async down(opts: { effects: T.Effects }) {
this.down(opts) this.down(opts)
} }
} }

View File

@@ -1,27 +1,31 @@
import { EmVer } from "../../emverLite/mod" import { ExtendedVersion } from "../../exver"
import { SDKManifest } from "../../manifest/ManifestTypes"
import { ExpectedExports } from "../../types" import * as T from "../../types"
import { once } from "../../util/once" import { once } from "../../util/once"
import { Migration } from "./Migration" import { Migration } from "./Migration"
export class Migrations<Manifest extends SDKManifest, Store> { export class Migrations<Manifest extends T.Manifest, Store> {
private constructor( private constructor(
readonly manifest: SDKManifest, readonly manifest: T.Manifest,
readonly migrations: Array<Migration<Manifest, Store, any>>, readonly migrations: Array<Migration<Manifest, Store, any>>,
) {} ) {}
private sortedMigrations = once(() => { private sortedMigrations = once(() => {
const migrationsAsVersions = ( const migrationsAsVersions = (
this.migrations as Array<Migration<Manifest, Store, any>> this.migrations as Array<Migration<Manifest, Store, any>>
).map((x) => [EmVer.parse(x.options.version), x] as const) )
.map((x) => [ExtendedVersion.parse(x.options.version), x] as const)
.filter(([v, _]) => v.flavor === this.currentVersion().flavor)
migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0]))
return migrationsAsVersions return migrationsAsVersions
}) })
private currentVersion = once(() => EmVer.parse(this.manifest.version)) private currentVersion = once(() =>
ExtendedVersion.parse(this.manifest.version),
)
static of< static of<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
Migrations extends Array<Migration<Manifest, Store, any>>, Migrations extends Array<Migration<Manifest, Store, any>>,
>(manifest: SDKManifest, ...migrations: EnsureUniqueId<Migrations>) { >(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
return new Migrations( return new Migrations(
manifest, manifest,
migrations as Array<Migration<Manifest, Store, any>>, migrations as Array<Migration<Manifest, Store, any>>,
@@ -30,11 +34,11 @@ export class Migrations<Manifest extends SDKManifest, Store> {
async init({ async init({
effects, effects,
previousVersion, previousVersion,
}: Parameters<ExpectedExports.init>[0]) { }: Parameters<T.ExpectedExports.init>[0]) {
if (!!previousVersion) { if (!!previousVersion) {
const previousVersionEmVer = EmVer.parse(previousVersion) const previousVersionExVer = ExtendedVersion.parse(previousVersion)
for (const [_, migration] of this.sortedMigrations() for (const [_, migration] of this.sortedMigrations()
.filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].greaterThan(previousVersionExVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.up({ effects }) await migration.up({ effects })
} }
@@ -43,12 +47,12 @@ export class Migrations<Manifest extends SDKManifest, Store> {
async uninit({ async uninit({
effects, effects,
nextVersion, nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) { }: Parameters<T.ExpectedExports.uninit>[0]) {
if (!!nextVersion) { if (!!nextVersion) {
const nextVersionEmVer = EmVer.parse(nextVersion) const nextVersionExVer = ExtendedVersion.parse(nextVersion)
const reversed = [...this.sortedMigrations()].reverse() const reversed = [...this.sortedMigrations()].reverse()
for (const [_, migration] of reversed for (const [_, migration] of reversed
.filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].greaterThan(nextVersionExVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.down({ effects }) await migration.down({ effects })
} }
@@ -57,10 +61,10 @@ export class Migrations<Manifest extends SDKManifest, Store> {
} }
export function setupMigrations< export function setupMigrations<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
Migrations extends Array<Migration<Manifest, Store, any>>, Migrations extends Array<Migration<Manifest, Store, any>>,
>(manifest: SDKManifest, ...migrations: EnsureUniqueId<Migrations>) { >(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
return Migrations.of<Manifest, Store, Migrations>(manifest, ...migrations) return Migrations.of<Manifest, Store, Migrations>(manifest, ...migrations)
} }

View File

@@ -1,25 +1,25 @@
import { DependenciesReceipt } from "../config/setupConfig" import { DependenciesReceipt } from "../config/setupConfig"
import { SetInterfaces } from "../interfaces/setupInterfaces" import { SetInterfaces } from "../interfaces/setupInterfaces"
import { SDKManifest } from "../manifest/ManifestTypes"
import { ExposedStorePaths } from "../store/setupExposeStore" import { ExposedStorePaths } from "../store/setupExposeStore"
import { Effects, ExpectedExports } from "../types" import * as T from "../types"
import { Migrations } from "./migrations/setupMigrations" import { Migrations } from "./migrations/setupMigrations"
import { Install } from "./setupInstall" import { Install } from "./setupInstall"
import { Uninstall } from "./setupUninstall" import { Uninstall } from "./setupUninstall"
export function setupInit<Manifest extends SDKManifest, Store>( export function setupInit<Manifest extends T.Manifest, Store>(
migrations: Migrations<Manifest, Store>, migrations: Migrations<Manifest, Store>,
install: Install<Manifest, Store>, install: Install<Manifest, Store>,
uninstall: Uninstall<Manifest, Store>, uninstall: Uninstall<Manifest, Store>,
setInterfaces: SetInterfaces<Manifest, Store, any, any>, setInterfaces: SetInterfaces<Manifest, Store, any, any>,
setDependencies: (options: { setDependencies: (options: {
effects: Effects effects: T.Effects
input: any input: any
}) => Promise<DependenciesReceipt>, }) => Promise<DependenciesReceipt>,
exposedStore: ExposedStorePaths, exposedStore: ExposedStorePaths,
): { ): {
init: ExpectedExports.init init: T.ExpectedExports.init
uninit: ExpectedExports.uninit uninit: T.ExpectedExports.uninit
} { } {
return { return {
init: async (opts) => { init: async (opts) => {

View File

@@ -1,12 +1,11 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Effects, ExpectedExports } from "../types"
export type InstallFn<Manifest extends SDKManifest, Store> = (opts: { export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
effects: Effects effects: T.Effects
}) => Promise<void> }) => Promise<void>
export class Install<Manifest extends SDKManifest, Store> { export class Install<Manifest extends T.Manifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {} private constructor(readonly fn: InstallFn<Manifest, Store>) {}
static of<Manifest extends SDKManifest, Store>( static of<Manifest extends T.Manifest, Store>(
fn: InstallFn<Manifest, Store>, fn: InstallFn<Manifest, Store>,
) { ) {
return new Install(fn) return new Install(fn)
@@ -15,7 +14,7 @@ export class Install<Manifest extends SDKManifest, Store> {
async init({ async init({
effects, effects,
previousVersion, previousVersion,
}: Parameters<ExpectedExports.init>[0]) { }: Parameters<T.ExpectedExports.init>[0]) {
if (!previousVersion) if (!previousVersion)
await this.fn({ await this.fn({
effects, effects,
@@ -23,7 +22,7 @@ export class Install<Manifest extends SDKManifest, Store> {
} }
} }
export function setupInstall<Manifest extends SDKManifest, Store>( export function setupInstall<Manifest extends T.Manifest, Store>(
fn: InstallFn<Manifest, Store>, fn: InstallFn<Manifest, Store>,
) { ) {
return Install.of(fn) return Install.of(fn)

View File

@@ -1,12 +1,11 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Effects, ExpectedExports } from "../types"
export type UninstallFn<Manifest extends SDKManifest, Store> = (opts: { export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
effects: Effects effects: T.Effects
}) => Promise<void> }) => Promise<void>
export class Uninstall<Manifest extends SDKManifest, Store> { export class Uninstall<Manifest extends T.Manifest, Store> {
private constructor(readonly fn: UninstallFn<Manifest, Store>) {} private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
static of<Manifest extends SDKManifest, Store>( static of<Manifest extends T.Manifest, Store>(
fn: UninstallFn<Manifest, Store>, fn: UninstallFn<Manifest, Store>,
) { ) {
return new Uninstall(fn) return new Uninstall(fn)
@@ -15,7 +14,7 @@ export class Uninstall<Manifest extends SDKManifest, Store> {
async uninit({ async uninit({
effects, effects,
nextVersion, nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) { }: Parameters<T.ExpectedExports.uninit>[0]) {
if (!nextVersion) if (!nextVersion)
await this.fn({ await this.fn({
effects, effects,
@@ -23,7 +22,7 @@ export class Uninstall<Manifest extends SDKManifest, Store> {
} }
} }
export function setupUninstall<Manifest extends SDKManifest, Store>( export function setupUninstall<Manifest extends T.Manifest, Store>(
fn: UninstallFn<Manifest, Store>, fn: UninstallFn<Manifest, Store>,
) { ) {
return Uninstall.of(fn) return Uninstall.of(fn)

View File

@@ -1,17 +1,17 @@
import { Config } from "../config/builder/config" import { Config } from "../config/builder/config"
import { SDKManifest } from "../manifest/ManifestTypes"
import { AddressInfo, Effects } from "../types" import * as T from "../types"
import { AddressReceipt } from "./AddressReceipt" import { AddressReceipt } from "./AddressReceipt"
export type InterfacesReceipt = Array<AddressInfo[] & AddressReceipt> export type InterfacesReceipt = Array<T.AddressInfo[] & AddressReceipt>
export type SetInterfaces< export type SetInterfaces<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
ConfigInput extends Record<string, any>, ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt, Output extends InterfacesReceipt,
> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise<Output> > = (opts: { effects: T.Effects; input: null | ConfigInput }) => Promise<Output>
export type SetupInterfaces = < export type SetupInterfaces = <
Manifest extends SDKManifest, Manifest extends T.Manifest,
Store, Store,
ConfigInput extends Record<string, any>, ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt, Output extends InterfacesReceipt,

View File

@@ -1,7 +1,7 @@
import { DEFAULT_SIGTERM_TIMEOUT } from "." import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk"
import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" import * as T from "../types"
import { MountOptions, Overlay } from "../util/Overlay" import { MountOptions, Overlay } from "../util/Overlay"
import { splitCommand } from "../util/splitCommand" import { splitCommand } from "../util/splitCommand"
import { cpExecFile, cpExec } from "./Daemons" import { cpExecFile, cpExec } from "./Daemons"
@@ -13,14 +13,14 @@ export class CommandController {
readonly pid: number | undefined, readonly pid: number | undefined,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {} ) {}
static of<Manifest extends SDKManifest>() { static of<Manifest extends T.Manifest>() {
return async <A extends string>( return async <A extends string>(
effects: Effects, effects: T.Effects,
imageId: { imageId: {
id: keyof Manifest["images"] & ImageId id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean sharedRun?: boolean
}, },
command: ValidIfNoStupidEscape<A> | [string, ...string[]], command: T.CommandType,
options: { options: {
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number sigtermTimeout?: number

View File

@@ -1,5 +1,4 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Effects, ImageId, ValidIfNoStupidEscape } from "../types"
import { MountOptions, Overlay } from "../util/Overlay" import { MountOptions, Overlay } from "../util/Overlay"
import { CommandController } from "./CommandController" import { CommandController } from "./CommandController"
@@ -14,14 +13,14 @@ export class Daemon {
private commandController: CommandController | null = null private commandController: CommandController | null = null
private shouldBeRunning = false private shouldBeRunning = false
private constructor(private startCommand: () => Promise<CommandController>) {} private constructor(private startCommand: () => Promise<CommandController>) {}
static of<Manifest extends SDKManifest>() { static of<Manifest extends T.Manifest>() {
return async <A extends string>( return async <A extends string>(
effects: Effects, effects: T.Effects,
imageId: { imageId: {
id: keyof Manifest["images"] & ImageId id: keyof Manifest["images"] & T.ImageId
sharedRun?: boolean sharedRun?: boolean
}, },
command: ValidIfNoStupidEscape<A> | [string, ...string[]], command: T.CommandType,
options: { options: {
mounts?: { path: string; options: MountOptions }[] mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay overlay?: Overlay

View File

@@ -1,16 +1,11 @@
import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk"
import { HealthReceipt } from "../health/HealthReceipt" import { HealthReceipt } from "../health/HealthReceipt"
import { CheckResult } from "../health/checkFns" import { CheckResult } from "../health/checkFns"
import { SDKManifest } from "../manifest/ManifestTypes"
import { Trigger } from "../trigger" import { Trigger } from "../trigger"
import { TriggerInput } from "../trigger/TriggerInput" import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../trigger/defaultTrigger" import { defaultTrigger } from "../trigger/defaultTrigger"
import { import * as T from "../types"
DaemonReturned,
Effects,
ImageId,
ValidIfNoStupidEscape,
} from "../types"
import { Mounts } from "./Mounts" import { Mounts } from "./Mounts"
import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" import { CommandOptions, MountOptions, Overlay } from "../util/Overlay"
import { splitCommand } from "../util/splitCommand" import { splitCommand } from "../util/splitCommand"
@@ -33,13 +28,13 @@ export type Ready = {
} }
type DaemonsParams< type DaemonsParams<
Manifest extends SDKManifest, Manifest extends T.Manifest,
Ids extends string, Ids extends string,
Command extends string, Command extends string,
Id extends string, Id extends string,
> = { > = {
command: ValidIfNoStupidEscape<Command> | [string, ...string[]] command: T.CommandType
image: { id: keyof Manifest["images"] & ImageId; sharedRun?: boolean } image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }
mounts: Mounts<Manifest> mounts: Mounts<Manifest>
env?: Record<string, string> env?: Record<string, string>
ready: Ready ready: Ready
@@ -49,7 +44,7 @@ type DaemonsParams<
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used` type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends SDKManifest>() => export const runCommand = <Manifest extends T.Manifest>() =>
CommandController.of<Manifest>() CommandController.of<Manifest>()
/** /**
@@ -75,9 +70,9 @@ Daemons.of({
}) })
``` ```
*/ */
export class Daemons<Manifest extends SDKManifest, Ids extends string> { export class Daemons<Manifest extends T.Manifest, Ids extends string> {
private constructor( private constructor(
readonly effects: Effects, readonly effects: T.Effects,
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>, readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>,
readonly daemons: Promise<Daemon>[], readonly daemons: Promise<Daemon>[],
readonly ids: Ids[], readonly ids: Ids[],
@@ -93,8 +88,8 @@ export class Daemons<Manifest extends SDKManifest, Ids extends string> {
* @param config * @param config
* @returns * @returns
*/ */
static of<Manifest extends SDKManifest>(config: { static of<Manifest extends T.Manifest>(config: {
effects: Effects effects: T.Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void> started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
healthReceipts: HealthReceipt[] healthReceipts: HealthReceipt[]
}) { }) {

View File

@@ -1,10 +1,9 @@
import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types"
import { Effects } from "../types"
import { MountOptions } from "../util/Overlay" import { MountOptions } from "../util/Overlay"
type MountArray = { path: string; options: MountOptions }[] type MountArray = { path: string; options: MountOptions }[]
export class Mounts<Manifest extends SDKManifest> { export class Mounts<Manifest extends T.Manifest> {
private constructor( private constructor(
readonly volumes: { readonly volumes: {
id: Manifest["volumes"][number] id: Manifest["volumes"][number]
@@ -26,7 +25,7 @@ export class Mounts<Manifest extends SDKManifest> {
}[], }[],
) {} ) {}
static of<Manifest extends SDKManifest>() { static of<Manifest extends T.Manifest>() {
return new Mounts<Manifest>([], [], []) return new Mounts<Manifest>([], [], [])
} }
@@ -58,7 +57,7 @@ export class Mounts<Manifest extends SDKManifest> {
return this return this
} }
addDependency<DependencyManifest extends SDKManifest>( addDependency<DependencyManifest extends T.Manifest>(
dependencyId: keyof Manifest["dependencies"] & string, dependencyId: keyof Manifest["dependencies"] & string,
volumeId: DependencyManifest["volumes"][number], volumeId: DependencyManifest["volumes"][number],
subpath: string | null, subpath: string | null,

View File

@@ -1,10 +1,10 @@
import { ExpectedExports } from "../types" import * as T from "../types"
import { Daemons } from "./Daemons" import { Daemons } from "./Daemons"
import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/ServiceInterfaceBuilder"
import "../interfaces/Origin" import "../interfaces/Origin"
import "./Daemons" import "./Daemons"
import { SDKManifest } from "../manifest/ManifestTypes"
import { MainEffects } from "../StartSdk" import { MainEffects } from "../StartSdk"
export const DEFAULT_SIGTERM_TIMEOUT = 30_000 export const DEFAULT_SIGTERM_TIMEOUT = 30_000
@@ -18,12 +18,12 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000
* @param fn * @param fn
* @returns * @returns
*/ */
export const setupMain = <Manifest extends SDKManifest, Store>( export const setupMain = <Manifest extends T.Manifest, Store>(
fn: (o: { fn: (o: {
effects: MainEffects effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void> started(onTerm: () => PromiseLike<void>): PromiseLike<void>
}) => Promise<Daemons<Manifest, any>>, }) => Promise<Daemons<Manifest, any>>,
): ExpectedExports.main => { ): T.ExpectedExports.main => {
return async (options) => { return async (options) => {
const result = await fn(options) const result = await fn(options)
return result return result

View File

@@ -1,20 +1,16 @@
import { ValidEmVer } from "../emverLite/mod" import { ValidateExVer, ValidateExVers } from "../exver"
import { ActionMetadata, ImageConfig, ImageId } from "../types" import {
ActionMetadata,
HardwareRequirements,
ImageConfig,
ImageId,
ImageSource,
} from "../types"
export type Container = { export type SDKManifest<
/** This should be pointing to a docker container name */ Version extends string,
image: string Satisfies extends string[] = [],
/** These should match the manifest data volumes */ > = {
mounts: Record<string, string>
/** Default is 64mb */
shmSizeMb?: `${number}${"mb" | "gb" | "b" | "kb"}`
/** if more than 30s to shutdown */
sigtermTimeout?: `${number}${"s" | "m" | "h"}`
}
export type ManifestVersion = ValidEmVer
export type SDKManifest = {
/** The package identifier used by the OS. This must be unique amongst all other known packages */ /** The package identifier used by the OS. This must be unique amongst all other known packages */
readonly id: string readonly id: string
/** A human readable service title */ /** A human readable service title */
@@ -23,7 +19,8 @@ export type SDKManifest = {
* - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of
* the service * the service
*/ */
readonly version: ManifestVersion readonly version: Version & ValidateExVer<Version>
readonly satisfies?: Satisfies & ValidateExVers<Satisfies>
/** Release notes for the update - can be a string, paragraph or URL */ /** Release notes for the update - can be a string, paragraph or URL */
readonly releaseNotes: string readonly releaseNotes: string
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
@@ -50,36 +47,49 @@ export type SDKManifest = {
} }
/** Defines the os images needed to run the container processes */ /** Defines the os images needed to run the container processes */
readonly images: Record<ImageId, ImageConfig> readonly images: Record<ImageId, SDKImageConfig>
/** This denotes readonly asset directories that should be available to mount to the container. /** This denotes readonly asset directories that should be available to mount to the container.
* Assuming that there will be three files with names along the lines: * These directories are expected to be found in `assets/<id>` at pack time.
* icon.* : the icon that will be this packages icon on the ui **/
* LICENSE : What the license is for this service
* Instructions : to be seen in the ui section of the package
* */
readonly assets: string[] readonly assets: string[]
/** This denotes any data volumes that should be available to mount to the container */ /** This denotes any data volumes that should be available to mount to the container */
readonly volumes: string[] readonly volumes: string[]
readonly alerts: { readonly alerts?: {
readonly install: string | null readonly install?: string | null
readonly update: string | null readonly update?: string | null
readonly uninstall: string | null readonly uninstall?: string | null
readonly restore: string | null readonly restore?: string | null
readonly start: string | null readonly start?: string | null
readonly stop: string | null readonly stop?: string | null
} }
readonly hasConfig?: boolean
readonly dependencies: Readonly<Record<string, ManifestDependency>> readonly dependencies: Readonly<Record<string, ManifestDependency>>
readonly hardwareRequirements?: {
readonly device?: { display?: RegExp; processor?: RegExp }
readonly ram?: number | null
readonly arch?: string[] | null
}
}
export type SDKImageConfig = {
source: Exclude<ImageSource, "packed">
arch?: string[]
emulateMissingAs?: string | null
} }
export type ManifestDependency = { export type ManifestDependency = {
/** /**
* A human readable explanation on what the dependency is used for * A human readable explanation on what the dependency is used for
*/ */
description: string | null readonly description: string | null
/** /**
* Determines if the dependency is optional or not. Times that optional that are good include such situations * Determines if the dependency is optional or not. Times that optional that are good include such situations
* such as being able to toggle other services or to use a different service for the same purpose. * such as being able to toggle other services or to use a different service for the same purpose.
*/ */
optional: boolean readonly optional: boolean
/**
* A url or local path for an s9pk that satisfies this dependency
*/
readonly s9pk: string
} }

View File

@@ -1,21 +1,71 @@
import * as T from "../types"
import { ImageConfig, ImageId, VolumeId } from "../osBindings" import { ImageConfig, ImageId, VolumeId } from "../osBindings"
import { SDKManifest, ManifestVersion } from "./ManifestTypes" import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
import { SDKVersion } from "../StartSdk"
export function setupManifest< export function setupManifest<
Id extends string, Id extends string,
Version extends ManifestVersion, Version extends string,
Dependencies extends Record<string, unknown>, Dependencies extends Record<string, unknown>,
VolumesTypes extends VolumeId, VolumesTypes extends VolumeId,
AssetTypes extends VolumeId, AssetTypes extends VolumeId,
ImagesTypes extends ImageId, ImagesTypes extends ImageId,
Manifest extends SDKManifest & { Manifest extends SDKManifest<Version, Satisfies> & {
dependencies: Dependencies dependencies: Dependencies
id: Id id: Id
version: Version
assets: AssetTypes[] assets: AssetTypes[]
images: Record<ImagesTypes, ImageConfig> images: Record<ImagesTypes, SDKImageConfig>
volumes: VolumesTypes[] volumes: VolumesTypes[]
}, },
>(manifest: Manifest): Manifest { Satisfies extends string[] = [],
return manifest >(manifest: Manifest & { version: Version }): Manifest & T.Manifest {
const images = Object.entries(manifest.images).reduce(
(images, [k, v]) => {
v.arch = v.arch || ["aarch64", "x86_64"]
if (v.emulateMissingAs === undefined)
v.emulateMissingAs = v.arch[0] || null
images[k] = v as ImageConfig
return images
},
{} as { [k: string]: ImageConfig },
)
return {
...manifest,
gitHash: null,
osVersion: SDKVersion,
satisfies: manifest.satisfies || [],
images,
alerts: {
install: manifest.alerts?.install || null,
update: manifest.alerts?.update || null,
uninstall: manifest.alerts?.uninstall || null,
restore: manifest.alerts?.restore || null,
start: manifest.alerts?.start || null,
stop: manifest.alerts?.stop || null,
},
hasConfig: manifest.hasConfig === undefined ? true : manifest.hasConfig,
hardwareRequirements: {
device: Object.fromEntries(
Object.entries(manifest.hardwareRequirements?.device || {}).map(
([k, v]) => [k, v.source],
),
),
ram: manifest.hardwareRequirements?.ram || null,
arch:
manifest.hardwareRequirements?.arch === undefined
? Object.values(images).reduce(
(arch, config) => {
if (config.emulateMissingAs) {
return arch
}
if (arch === null) {
return config.arch
}
return arch.filter((a) => config.arch.includes(a))
},
null as string[] | null,
)
: manifest.hardwareRequirements?.arch,
},
}
} }

View File

@@ -1,4 +1,5 @@
// 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 { HealthCheckId } from "./HealthCheckId"
import type { HealthCheckResult } from "./HealthCheckResult" import type { HealthCheckResult } from "./HealthCheckResult"
import type { PackageId } from "./PackageId" import type { PackageId } from "./PackageId"
@@ -6,6 +7,7 @@ export type CheckDependenciesResult = {
packageId: PackageId packageId: PackageId
isInstalled: boolean isInstalled: boolean
isRunning: boolean isRunning: boolean
healthChecks: Array<HealthCheckResult> configSatisfied: boolean
healthChecks: { [key: HealthCheckId]: HealthCheckResult }
version: string | null version: string | null
} }

View File

@@ -2,9 +2,8 @@
import type { DataUrl } from "./DataUrl" import type { DataUrl } from "./DataUrl"
export type CurrentDependencyInfo = { export type CurrentDependencyInfo = {
title: string title: string | null
icon: DataUrl icon: DataUrl | null
registryUrl: string versionRange: string
versionSpec: string
configSatisfied: boolean configSatisfied: boolean
} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] })

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.
import type { PathOrUrl } from "./PathOrUrl"
export type DepInfo = { description: string | null; optional: boolean } export type DepInfo = {
description: string | null
optional: boolean
s9pk: PathOrUrl | null
}

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 { DataUrl } from "./DataUrl"
export type DependencyMetadata = {
title: string | null
icon: DataUrl | null
description: string | null
optional: boolean
}

View File

@@ -5,7 +5,6 @@ export type DependencyRequirement =
kind: "running" kind: "running"
id: string id: string
healthChecks: string[] healthChecks: string[]
versionSpec: string versionRange: string
registryUrl: string
} }
| { kind: "exists"; id: string; versionSpec: string; registryUrl: string } | { kind: "exists"; id: string; versionRange: string }

View File

@@ -6,6 +6,7 @@ import type { PackageIndex } from "./PackageIndex"
import type { SignerInfo } from "./SignerInfo" import type { SignerInfo } from "./SignerInfo"
export type FullIndex = { export type FullIndex = {
name: string | null
icon: DataUrl | null icon: DataUrl | null
package: PackageIndex package: PackageIndex
os: OsIndex os: OsIndex

View File

@@ -1,6 +1,6 @@
// 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 GetVersionParams = { export type GetOsVersionParams = {
source: string | null source: string | null
target: string | null target: string | null
serverId: string | null serverId: string | null

View File

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

View File

@@ -1,7 +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 HardwareRequirements = { export type HardwareRequirements = {
device: { [key: string]: string } device: { device?: string; processor?: string }
ram: number | null ram: number | null
arch: string[] | null arch: string[] | null
} }

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 InstallParams = {
registry: string
id: PackageId
version: Version
}

View File

@@ -13,6 +13,7 @@ export type Manifest = {
id: PackageId id: PackageId
title: string title: string
version: Version version: Version
satisfies: Array<Version>
releaseNotes: string releaseNotes: string
license: string license: string
wrapperRepo: string wrapperRepo: string

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PackageDetailLevel = "short" | "full" export type PackageDetailLevel = "none" | "short" | "full"

View File

@@ -1,8 +1,11 @@
// 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 { Alerts } from "./Alerts"
import type { DataUrl } from "./DataUrl" import type { DataUrl } from "./DataUrl"
import type { DependencyMetadata } from "./DependencyMetadata"
import type { Description } from "./Description" import type { Description } from "./Description"
import type { HardwareRequirements } from "./HardwareRequirements" import type { HardwareRequirements } from "./HardwareRequirements"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
import type { PackageId } from "./PackageId"
import type { RegistryAsset } from "./RegistryAsset" import type { RegistryAsset } from "./RegistryAsset"
export type PackageVersionInfo = { export type PackageVersionInfo = {
@@ -16,6 +19,9 @@ export type PackageVersionInfo = {
upstreamRepo: string upstreamRepo: string
supportSite: string supportSite: string
marketingSite: string marketingSite: string
donationUrl: string | null
alerts: Alerts
dependencyMetadata: { [key: PackageId]: DependencyMetadata }
osVersion: string osVersion: string
hardwareRequirements: HardwareRequirements hardwareRequirements: HardwareRequirements
sourceVersion: string | null sourceVersion: string | null

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PathOrUrl = string

View File

@@ -3,6 +3,7 @@ import type { AnySignature } from "./AnySignature"
import type { AnyVerifyingKey } from "./AnyVerifyingKey" import type { AnyVerifyingKey } from "./AnyVerifyingKey"
export type RegistryAsset<Commitment> = { export type RegistryAsset<Commitment> = {
publishedAt: string
url: string url: 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 { Category } from "./Category"
import type { DataUrl } from "./DataUrl"
export type RegistryInfo = {
name: string | null
icon: DataUrl | null
categories: { [key: string]: Category }
}

View File

@@ -37,6 +37,7 @@ export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
export { DataUrl } from "./DataUrl" export { DataUrl } from "./DataUrl"
export { Dependencies } from "./Dependencies" export { Dependencies } from "./Dependencies"
export { DependencyKind } from "./DependencyKind" export { DependencyKind } from "./DependencyKind"
export { DependencyMetadata } from "./DependencyMetadata"
export { DependencyRequirement } from "./DependencyRequirement" export { DependencyRequirement } from "./DependencyRequirement"
export { DepInfo } from "./DepInfo" export { DepInfo } from "./DepInfo"
export { Description } from "./Description" export { Description } from "./Description"
@@ -51,6 +52,7 @@ export { FullIndex } from "./FullIndex"
export { FullProgress } from "./FullProgress" export { FullProgress } from "./FullProgress"
export { GetHostInfoParams } from "./GetHostInfoParams" export { GetHostInfoParams } from "./GetHostInfoParams"
export { GetOsAssetParams } from "./GetOsAssetParams" export { GetOsAssetParams } from "./GetOsAssetParams"
export { GetOsVersionParams } from "./GetOsVersionParams"
export { GetPackageParams } from "./GetPackageParams" export { GetPackageParams } from "./GetPackageParams"
export { GetPackageResponseFull } from "./GetPackageResponseFull" export { GetPackageResponseFull } from "./GetPackageResponseFull"
export { GetPackageResponse } from "./GetPackageResponse" export { GetPackageResponse } from "./GetPackageResponse"
@@ -61,7 +63,6 @@ export { GetSslCertificateParams } from "./GetSslCertificateParams"
export { GetSslKeyParams } from "./GetSslKeyParams" export { GetSslKeyParams } from "./GetSslKeyParams"
export { GetStoreParams } from "./GetStoreParams" export { GetStoreParams } from "./GetStoreParams"
export { GetSystemSmtpParams } from "./GetSystemSmtpParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams"
export { GetVersionParams } from "./GetVersionParams"
export { Governor } from "./Governor" export { Governor } from "./Governor"
export { Guid } from "./Guid" export { Guid } from "./Guid"
export { HardwareRequirements } from "./HardwareRequirements" export { HardwareRequirements } from "./HardwareRequirements"
@@ -82,6 +83,7 @@ export { InstalledState } from "./InstalledState"
export { InstalledVersionParams } from "./InstalledVersionParams" export { InstalledVersionParams } from "./InstalledVersionParams"
export { InstallingInfo } from "./InstallingInfo" export { InstallingInfo } from "./InstallingInfo"
export { InstallingState } from "./InstallingState" export { InstallingState } from "./InstallingState"
export { InstallParams } from "./InstallParams"
export { IpHostname } from "./IpHostname" export { IpHostname } from "./IpHostname"
export { IpInfo } from "./IpInfo" export { IpInfo } from "./IpInfo"
export { LanInfo } from "./LanInfo" export { LanInfo } from "./LanInfo"
@@ -109,11 +111,13 @@ export { PackageVersionInfo } from "./PackageVersionInfo"
export { ParamsMaybePackageId } from "./ParamsMaybePackageId" export { ParamsMaybePackageId } from "./ParamsMaybePackageId"
export { ParamsPackageId } from "./ParamsPackageId" export { ParamsPackageId } from "./ParamsPackageId"
export { PasswordType } from "./PasswordType" export { PasswordType } from "./PasswordType"
export { PathOrUrl } from "./PathOrUrl"
export { ProcedureId } from "./ProcedureId" export { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress" export { Progress } from "./Progress"
export { Public } from "./Public" export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource" export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset" export { RegistryAsset } from "./RegistryAsset"
export { RegistryInfo } from "./RegistryInfo"
export { RemoveActionParams } from "./RemoveActionParams" export { RemoveActionParams } from "./RemoveActionParams"
export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveAddressParams } from "./RemoveAddressParams"
export { RemoveVersionParams } from "./RemoveVersionParams" export { RemoveVersionParams } from "./RemoveVersionParams"

View File

@@ -369,7 +369,7 @@ describe("values", () => {
setupManifest({ setupManifest({
id: "testOutput", id: "testOutput",
title: "", title: "",
version: "1.0", version: "1.0.0:0",
releaseNotes: "", releaseNotes: "",
license: "", license: "",
replaces: [], replaces: [],
@@ -395,9 +395,10 @@ describe("values", () => {
stop: null, stop: null,
}, },
dependencies: { dependencies: {
remoteTest: { "remote-test": {
description: "", description: "",
optional: true, optional: true,
s9pk: "https://example.com/remote-test.s9pk",
}, },
}, },
}), }),

View File

@@ -1,253 +0,0 @@
import { EmVer, notRange, rangeAnd, rangeOf, rangeOr } from "../emverLite/mod"
describe("EmVer", () => {
{
{
const checker = rangeOf("*")
test("rangeOf('*')", () => {
checker.check("1")
checker.check("1.2")
checker.check("1.2.3")
checker.check("1.2.3.4")
checker.check("1.2.3.4.5")
checker.check("1.2.3.4.5.6")
expect(checker.check("1")).toEqual(true)
expect(checker.check("1.2")).toEqual(true)
expect(checker.check("1.2.3.4")).toEqual(true)
})
test("rangeOf('*') invalid", () => {
expect(() => checker.check("a")).toThrow()
expect(() => checker.check("")).toThrow()
expect(() => checker.check("1..3")).toThrow()
})
}
{
const checker = rangeOf(">1.2.3.4")
test(`rangeOf(">1.2.3.4") valid`, () => {
expect(checker.check("2-beta123")).toEqual(true)
expect(checker.check("2")).toEqual(true)
expect(checker.check("1.2.3.5")).toEqual(true)
expect(checker.check("1.2.3.4.1")).toEqual(true)
})
test(`rangeOf(">1.2.3.4") invalid`, () => {
expect(checker.check("1.2.3.4")).toEqual(false)
expect(checker.check("1.2.3")).toEqual(false)
expect(checker.check("1")).toEqual(false)
})
}
{
const checker = rangeOf("=1.2.3")
test(`rangeOf("=1.2.3") valid`, () => {
expect(checker.check("1.2.3")).toEqual(true)
})
test(`rangeOf("=1.2.3") invalid`, () => {
expect(checker.check("2")).toEqual(false)
expect(checker.check("1.2.3.1")).toEqual(false)
expect(checker.check("1.2")).toEqual(false)
})
}
{
const checker = rangeOf(">=1.2.3.4")
test(`rangeOf(">=1.2.3.4") valid`, () => {
expect(checker.check("2")).toEqual(true)
expect(checker.check("1.2.3.5")).toEqual(true)
expect(checker.check("1.2.3.4.1")).toEqual(true)
expect(checker.check("1.2.3.4")).toEqual(true)
})
test(`rangeOf(">=1.2.3.4") invalid`, () => {
expect(checker.check("1.2.3")).toEqual(false)
expect(checker.check("1")).toEqual(false)
})
}
{
const checker = rangeOf("<1.2.3.4")
test(`rangeOf("<1.2.3.4") invalid`, () => {
expect(checker.check("2")).toEqual(false)
expect(checker.check("1.2.3.5")).toEqual(false)
expect(checker.check("1.2.3.4.1")).toEqual(false)
expect(checker.check("1.2.3.4")).toEqual(false)
})
test(`rangeOf("<1.2.3.4") valid`, () => {
expect(checker.check("1.2.3")).toEqual(true)
expect(checker.check("1")).toEqual(true)
})
}
{
const checker = rangeOf("<=1.2.3.4")
test(`rangeOf("<=1.2.3.4") invalid`, () => {
expect(checker.check("2")).toEqual(false)
expect(checker.check("1.2.3.5")).toEqual(false)
expect(checker.check("1.2.3.4.1")).toEqual(false)
})
test(`rangeOf("<=1.2.3.4") valid`, () => {
expect(checker.check("1.2.3")).toEqual(true)
expect(checker.check("1")).toEqual(true)
expect(checker.check("1.2.3.4")).toEqual(true)
})
}
{
const checkA = rangeOf(">1")
const checkB = rangeOf("<=2")
const checker = rangeAnd(checkA, checkB)
test(`simple and(checkers) valid`, () => {
expect(checker.check("2")).toEqual(true)
expect(checker.check("1.1")).toEqual(true)
})
test(`simple and(checkers) invalid`, () => {
expect(checker.check("2.1")).toEqual(false)
expect(checker.check("1")).toEqual(false)
expect(checker.check("0")).toEqual(false)
})
}
{
const checkA = rangeOf("<1")
const checkB = rangeOf("=2")
const checker = rangeOr(checkA, checkB)
test(`simple or(checkers) valid`, () => {
expect(checker.check("2")).toEqual(true)
expect(checker.check("0.1")).toEqual(true)
})
test(`simple or(checkers) invalid`, () => {
expect(checker.check("2.1")).toEqual(false)
expect(checker.check("1")).toEqual(false)
expect(checker.check("1.1")).toEqual(false)
})
}
{
const checker = rangeOf("1.2.*")
test(`rangeOf(1.2.*) valid`, () => {
expect(checker.check("1.2")).toEqual(true)
expect(checker.check("1.2.1")).toEqual(true)
})
test(`rangeOf(1.2.*) invalid`, () => {
expect(checker.check("1.3")).toEqual(false)
expect(checker.check("1.3.1")).toEqual(false)
expect(checker.check("1.1.1")).toEqual(false)
expect(checker.check("1.1")).toEqual(false)
expect(checker.check("1")).toEqual(false)
expect(checker.check("2")).toEqual(false)
})
}
{
const checker = notRange(rangeOf("1.2.*"))
test(`notRange(rangeOf(1.2.*)) valid`, () => {
expect(checker.check("1.3")).toEqual(true)
expect(checker.check("1.3.1")).toEqual(true)
expect(checker.check("1.1.1")).toEqual(true)
expect(checker.check("1.1")).toEqual(true)
expect(checker.check("1")).toEqual(true)
expect(checker.check("2")).toEqual(true)
})
test(`notRange(rangeOf(1.2.*)) invalid `, () => {
expect(checker.check("1.2")).toEqual(false)
expect(checker.check("1.2.1")).toEqual(false)
})
}
{
const checker = rangeOf("!1.2.*")
test(`!(rangeOf(1.2.*)) valid`, () => {
expect(checker.check("1.3")).toEqual(true)
expect(checker.check("1.3.1")).toEqual(true)
expect(checker.check("1.1.1")).toEqual(true)
expect(checker.check("1.1")).toEqual(true)
expect(checker.check("1")).toEqual(true)
expect(checker.check("2")).toEqual(true)
})
test(`!(rangeOf(1.2.*)) invalid `, () => {
expect(checker.check("1.2")).toEqual(false)
expect(checker.check("1.2.1")).toEqual(false)
})
}
{
test(`no and ranges`, () => {
expect(() => rangeAnd()).toThrow()
})
test(`no or ranges`, () => {
expect(() => rangeOr()).toThrow()
})
}
{
const checker = rangeOf("!>1.2.3.4")
test(`rangeOf("!>1.2.3.4") invalid`, () => {
expect(checker.check("2")).toEqual(false)
expect(checker.check("1.2.3.5")).toEqual(false)
expect(checker.check("1.2.3.4.1")).toEqual(false)
})
test(`rangeOf("!>1.2.3.4") valid`, () => {
expect(checker.check("1.2.3.4")).toEqual(true)
expect(checker.check("1.2.3")).toEqual(true)
expect(checker.check("1")).toEqual(true)
})
}
{
test(">1 && =1.2", () => {
const checker = rangeOf(">1 && =1.2")
expect(checker.check("1.2")).toEqual(true)
expect(checker.check("1.2.1")).toEqual(false)
})
test("=1 || =2", () => {
const checker = rangeOf("=1 || =2")
expect(checker.check("1")).toEqual(true)
expect(checker.check("2")).toEqual(true)
expect(checker.check("3")).toEqual(false)
})
test(">1 && =1.2 || =2", () => {
const checker = rangeOf(">1 && =1.2 || =2")
expect(checker.check("1.2")).toEqual(true)
expect(checker.check("1")).toEqual(false)
expect(checker.check("2")).toEqual(true)
expect(checker.check("3")).toEqual(false)
})
test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => {
const checker = rangeOf("<1.5 && >1 || >1.5 && <3")
expect(checker.check("1.1")).toEqual(true)
expect(checker.check("2")).toEqual(true)
expect(checker.check("1.5")).toEqual(false)
expect(checker.check("1")).toEqual(false)
expect(checker.check("3")).toEqual(false)
})
test("Compare function on the emver", () => {
const a = EmVer.from("1.2.3")
const b = EmVer.from("1.2.4")
expect(a.compare(b)).toEqual("less")
expect(b.compare(a)).toEqual("greater")
expect(a.compare(a)).toEqual("equal")
})
test("Compare for sort function on the emver", () => {
const a = EmVer.from("1.2.3")
const b = EmVer.from("1.2.4")
expect(a.compareForSort(b)).toEqual(-1)
expect(b.compareForSort(a)).toEqual(1)
expect(a.compareForSort(a)).toEqual(0)
})
}
}
})

View File

@@ -0,0 +1,355 @@
import { VersionRange, ExtendedVersion } from "../exver"
describe("ExVer", () => {
{
{
const checker = VersionRange.parse("*")
test("VersionRange.parse('*')", () => {
checker.satisfiedBy(ExtendedVersion.parse("1:0"))
checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))
checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))
checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))
checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5"))
checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5.6"))
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
true,
)
})
test("VersionRange.parse('*') invalid", () => {
expect(() => checker.satisfiedBy(ExtendedVersion.parse("a"))).toThrow()
expect(() => checker.satisfiedBy(ExtendedVersion.parse(""))).toThrow()
expect(() =>
checker.satisfiedBy(ExtendedVersion.parse("1..3")),
).toThrow()
})
}
{
const checker = VersionRange.parse(">1.2.3:4")
test(`VersionRange.parse(">1.2.3:4") valid`, () => {
expect(
checker.satisfiedBy(ExtendedVersion.parse("2-beta.123:0")),
).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual(
true,
)
})
test(`VersionRange.parse(">1.2.3:4") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
})
}
{
const checker = VersionRange.parse("=1.2.3")
test(`VersionRange.parse("=1.2.3") valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
true,
)
})
test(`VersionRange.parse("=1.2.3") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:1"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
false,
)
})
}
{
const checker = VersionRange.parse(">=1.2.3:4")
test(`VersionRange.parse(">=1.2.3:4") valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
true,
)
})
test(`VersionRange.parse(">=1.2.3:4") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
})
}
{
const checker = VersionRange.parse("<1.2.3:4")
test(`VersionRange.parse("<1.2.3:4") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
false,
)
})
test(`VersionRange.parse("<1.2.3:4") valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
})
}
{
const checker = VersionRange.parse("<=1.2.3:4")
test(`VersionRange.parse("<=1.2.3:4") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual(
false,
)
})
test(`VersionRange.parse("<=1.2.3:4") valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
true,
)
})
}
{
const checkA = VersionRange.parse(">1")
const checkB = VersionRange.parse("<=2")
const checker = checkA.and(checkB)
test(`simple and(checkers) valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
true,
)
})
test(`simple and(checkers) invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
})
}
{
const checkA = VersionRange.parse("<1")
const checkB = VersionRange.parse("=2")
const checker = checkA.or(checkB)
test(`simple or(checkers) valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("0.1:0"))).toEqual(
true,
)
})
test(`simple or(checkers) invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
false,
)
})
}
{
const checker = VersionRange.parse("~1.2")
test(`VersionRange.parse(~1.2) valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual(
true,
)
})
test(`VersionRange.parse(~1.2) invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false)
})
}
{
const checker = VersionRange.parse("~1.2").not()
test(`VersionRange.parse(~1.2).not() valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
})
test(`VersionRange.parse(~1.2).not() invalid `, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual(
false,
)
})
}
{
const checker = VersionRange.parse("!~1.2")
test(`!(VersionRange.parse(~1.2)) valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
})
test(`!(VersionRange.parse(~1.2)) invalid `, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual(
false,
)
})
}
{
const checker = VersionRange.parse("!>1.2.3:4")
test(`VersionRange.parse("!>1.2.3:4") invalid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual(
false,
)
})
test(`VersionRange.parse("!>1.2.3:4") valid`, () => {
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
})
}
{
test(">1 && =1.2", () => {
const checker = VersionRange.parse(">1 && =1.2")
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual(
false,
)
})
test("=1 || =2", () => {
const checker = VersionRange.parse("=1 || =2")
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false)
})
test(">1 && =1.2 || =2", () => {
const checker = VersionRange.parse(">1 && =1.2 || =2")
expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false)
})
test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => {
const checker = VersionRange.parse("<1.5 && >1 || >1.5 && <3")
expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual(
true,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true)
expect(checker.satisfiedBy(ExtendedVersion.parse("1.5:0"))).toEqual(
false,
)
expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false)
expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false)
})
test("Compare function on the emver", () => {
const a = ExtendedVersion.parse("1.2.3:0")
const b = ExtendedVersion.parse("1.2.4:0")
expect(a.compare(b)).toEqual("less")
expect(b.compare(a)).toEqual("greater")
expect(a.compare(a)).toEqual("equal")
})
test("Compare for sort function on the emver", () => {
const a = ExtendedVersion.parse("1.2.3:0")
const b = ExtendedVersion.parse("1.2.4:0")
expect(a.compareForSort(b)).toEqual(-1)
expect(b.compareForSort(a)).toEqual(1)
expect(a.compareForSort(a)).toEqual(0)
})
}
}
})

View File

@@ -7,7 +7,7 @@ export const sdk = StartSdk.of()
setupManifest({ setupManifest({
id: "testOutput", id: "testOutput",
title: "", title: "",
version: "1.0", version: "1.0:0",
releaseNotes: "", releaseNotes: "",
license: "", license: "",
replaces: [], replaces: [],
@@ -33,9 +33,10 @@ export const sdk = StartSdk.of()
stop: null, stop: null,
}, },
dependencies: { dependencies: {
remoteTest: { "remote-test": {
description: "", description: "",
optional: false, optional: false,
s9pk: "https://example.com/remote-test.s9pk",
}, },
}, },
}), }),

View File

@@ -21,7 +21,7 @@ describe("setupDependencyConfig", () => {
dependencyConfig: async ({}) => {}, dependencyConfig: async ({}) => {},
}) })
sdk.setupDependencyConfig(testConfig, { sdk.setupDependencyConfig(testConfig, {
remoteTest, "remote-test": remoteTest,
}) })
}) })
}) })

View File

@@ -1,42 +0,0 @@
import { getHostname } from "../util/getServiceInterface"
import { splitCommand } from "../util/splitCommand"
describe("splitCommand ", () => {
const inputToExpected = [
["cat", ["cat"]],
[["cat"], ["cat"]],
[
["cat", "hello all my homies"],
["cat", "hello all my homies"],
],
["cat hello world", ["cat", "hello", "world"]],
["cat hello 'big world'", ["cat", "hello", "big world"]],
[`cat hello "big world"`, ["cat", "hello", "big world"]],
[
`cat hello "big world's are the greatest"`,
["cat", "hello", "big world's are the greatest"],
],
// Too many spaces
["cat ", ["cat"]],
[["cat "], ["cat "]],
[
["cat ", "hello all my homies "],
["cat ", "hello all my homies "],
],
["cat hello world ", ["cat", "hello", "world"]],
[
" cat hello 'big world' ",
["cat", "hello", "big world"],
],
[
` cat hello "big world" `,
["cat", "hello", "big world"],
],
]
for (const [input, expectValue] of inputToExpected) {
test(`should return ${expectValue} for ${input}`, () => {
expect(splitCommand(input as any)).toEqual(expectValue)
})
}
})

View File

@@ -12,6 +12,7 @@ import {
LanInfo, LanInfo,
BindParams, BindParams,
Manifest, Manifest,
CheckDependenciesResult,
} from "./osBindings" } from "./osBindings"
import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk"
@@ -154,14 +155,6 @@ export type DependencyConfig = {
}): Promise<unknown> }): Promise<unknown>
} }
export type ValidIfNoStupidEscape<A> = A extends
| `${string}'"'"'${string}`
| `${string}\\"${string}`
? never
: "" extends A & ""
? never
: A
export type ConfigRes = { export type ConfigRes = {
/** This should be the previous config, that way during set config we start with the previous */ /** This should be the previous config, that way during set config we start with the previous */
config?: null | Record<string, unknown> config?: null | Record<string, unknown>
@@ -188,9 +181,7 @@ export type SmtpValue = {
password: string | null | undefined password: string | null | undefined
} }
export type CommandType<A extends string> = export type CommandType = string | [string, ...string[]]
| ValidIfNoStupidEscape<A>
| [string, ...string[]]
export type DaemonReturned = { export type DaemonReturned = {
wait(): Promise<unknown> wait(): Promise<unknown>
@@ -470,7 +461,7 @@ export type Effects = {
*/ */
checkDependencies(options: { checkDependencies(options: {
packageIds: PackageId[] | null packageIds: PackageId[] | null
}): Promise<CheckDependencyResult[]> }): Promise<CheckDependenciesResult[]>
/** Exists could be useful during the runtime to know if some service exists, option dep */ /** Exists could be useful during the runtime to know if some service exists, option dep */
exists(options: { packageId: PackageId }): Promise<boolean> exists(options: { packageId: PackageId }): Promise<boolean>
/** Exists could be useful during the runtime to know if some service is running, option dep */ /** Exists could be useful during the runtime to know if some service is running, option dep */
@@ -554,12 +545,3 @@ export type Dependencies = Array<DependencyRequirement>
export type DeepPartial<T> = T extends {} export type DeepPartial<T> = T extends {}
? { [P in keyof T]?: DeepPartial<T[P]> } ? { [P in keyof T]?: DeepPartial<T[P]> }
: T : T
export type CheckDependencyResult = {
packageId: PackageId
isInstalled: boolean
isRunning: boolean
healthChecks: SetHealth[]
version: string | null
}
export type CheckResults = CheckDependencyResult[]

View File

@@ -1,17 +1,8 @@
import { arrayOf, string } from "ts-matches" import { arrayOf, string } from "ts-matches"
import { ValidIfNoStupidEscape } from "../types"
export const splitCommand = ( export const splitCommand = (
command: string | [string, ...string[]], command: string | [string, ...string[]],
): string[] => { ): string[] => {
if (arrayOf(string).test(command)) return command if (arrayOf(string).test(command)) return command
return String(command) return ["sh", "-c", command]
.split('"')
.flatMap((x, i) =>
i % 2 !== 0
? [x]
: x.split("'").flatMap((x, i) => (i % 2 !== 0 ? [x] : x.split(" "))),
)
.map((x) => x.trim())
.filter(Boolean)
} }

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