diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index 725e8d22c..ed975f25e 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -36,7 +36,7 @@ let hostSystemId = 0 export type EffectContext = { procedureId: string | null callbacks?: CallbackHolder - constRetry: () => void + constRetry?: () => void } const rpcRoundFor = diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index c2dc8bafe..dade89ad2 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -306,7 +306,6 @@ export class RpcListener { const effects = makeEffects({ procedureId: null, callbacks, - constRetry: () => {}, }) return handleRpc( id, @@ -337,7 +336,6 @@ export class RpcListener { this.callbacks = new CallbackHolder( makeEffects({ procedureId: null, - constRetry: () => {}, }), ) const callbacks = this.callbackHolderFor("containerInit") @@ -345,7 +343,6 @@ export class RpcListener { makeEffects({ procedureId: null, callbacks, - constRetry: () => {}, }), ) this._system = system @@ -427,7 +424,6 @@ export class RpcListener { const effects = makeEffects({ procedureId, callbacks, - constRetry: () => {}, }) return (async () => { diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 806216786..941e21ac8 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -62,7 +62,7 @@ export class DockerProcedureContainer { ) } else if (volumeMount.type === "assets") { await subcontainer.mount( - { type: "assets", id: mount, subpath: null }, + { type: "assets", subpath: mount }, mounts[mount], ) } else if (volumeMount.type === "certificate") { diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index bec5b092c..0099bea92 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -170,7 +170,7 @@ export const polyfillEffects = ( { mounts: [ { - path: "/drive", + mountpoint: "/drive", options: { type: "volume", id: input.volumeId, @@ -212,7 +212,7 @@ export const polyfillEffects = ( { mounts: [ { - path: "/drive", + mountpoint: "/drive", options: { type: "volume", id: input.volumeId, diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/network_interface.rs index 00c5fc56b..6fbc519ee 100644 --- a/core/startos/src/net/network_interface.rs +++ b/core/startos/src/net/network_interface.rs @@ -1034,6 +1034,15 @@ impl ListenerMap { fn poll_accept(&self, cx: &mut std::task::Context<'_>) -> Poll> { for (bind_addr, listener) in self.listeners.iter() { if let Poll::Ready((stream, addr)) = listener.0.poll_accept(cx)? { + if let Err(e) = socket2::SockRef::from(&stream).set_tcp_keepalive( + &socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(900)) + .with_interval(Duration::from_secs(60)) + .with_retries(5), + ) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } return Poll::Ready(Ok(Accepted { stream, peer: addr, diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 3a4329693..f28712304 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -258,16 +258,6 @@ impl VHostServer { } } - if let Err(e) = socket2::SockRef::from(&accepted.stream).set_tcp_keepalive( - &socket2::TcpKeepalive::new() - .with_time(Duration::from_secs(900)) - .with_interval(Duration::from_secs(60)) - .with_retries(5), - ) { - tracing::error!("Failed to set tcp keepalive: {e}"); - tracing::debug!("{e:?}"); - } - tokio::spawn(async move { let bind = accepted.bind; if let Err(e) = diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs index c9a2fd31b..71088f243 100644 --- a/core/startos/src/s9pk/merkle_archive/expected.rs +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -36,6 +36,25 @@ impl<'a, T: Clone> Expected<'a, T> { )) } } + pub fn check_dir(&mut self, path: impl AsRef) -> Result<(), Error> { + if let Some(dir) = self + .dir + .get_path(path.as_ref()) + .and_then(|e| e.as_directory()) + { + for entry in dir.file_paths(path.as_ref()) { + if !entry.to_string_lossy().ends_with("/") { + self.keep.insert_path(entry, Entry::file(()))?; + } + } + Ok(()) + } else { + Err(Error::new( + eyre!("directory {} missing from archive", path.as_ref().display()), + ErrorKind::ParseS9pk, + )) + } + } pub fn check_stem( &mut self, path: impl AsRef, diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index db8cbc414..e1253a373 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -129,25 +129,16 @@ impl S9pk> { tokio_tar::Archive::new(reader.assets().await?) .unpack(&asset_dir) .await?; - for (asset_id, _) in manifest - .volumes - .iter() - .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets")) - { - let assets_path = asset_dir.join(&asset_id); - let sqfs_path = assets_path.with_extension("squashfs"); - Command::new("mksquashfs") - .arg(&assets_path) - .arg(&sqfs_path) - .invoke(ErrorKind::Filesystem) - .await?; - archive.insert_path( - Path::new("assets") - .join(&asset_id) - .with_extension("squashfs"), - Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), - )?; - } + let sqfs_path = asset_dir.with_extension("squashfs"); + Command::new("mksquashfs") + .arg(&asset_dir) + .arg(&sqfs_path) + .invoke(ErrorKind::Filesystem) + .await?; + archive.insert_path( + "assets.squashfs", + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), + )?; // javascript let js_dir = tmp_dir.join("javascript"); @@ -217,12 +208,6 @@ impl TryFrom for Manifest { donation_url: value.donation_url, description: value.description, images: BTreeMap::new(), - assets: value - .volumes - .iter() - .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets")) - .map(|(id, _)| id.clone()) - .collect(), volumes: value .volumes .iter() diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 11ea0d9af..6b3b11bd9 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -53,7 +53,6 @@ pub struct Manifest { pub donation_url: Option, pub description: Description, pub images: BTreeMap, - pub assets: BTreeSet, // TODO: AssetsId pub volumes: BTreeSet, #[serde(default)] pub alerts: Alerts, @@ -93,8 +92,11 @@ impl Manifest { .map_or(false, |mime| mime.starts_with("image/")) }); } - for assets in &self.assets { - expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; + if let Err(e) = expected.check_file(Path::new("assets.squashfs")) { + // backwards compatibility for alpha s9pks - remove eventually + if expected.check_dir("assets").is_err() { + return Err(e); + } } for (image_id, config) in &self.images { let mut check_arch = |arch: &str| { diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index ecab202ce..852e7a21d 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -60,7 +60,7 @@ fn priority(s: &str) -> Option { "instructions.md" => Some(3), "dependencies" => Some(4), "javascript.squashfs" => Some(5), - "assets" => Some(6), + "assets.squashfs" => Some(6), "images" => Some(7), _ => None, } diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index 7117b4c3b..faf163988 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -694,18 +694,13 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { .await?; let assets_dir = params.assets(); - for assets in s9pk.as_manifest().assets.clone() { - s9pk.as_archive_mut().contents_mut().insert_path( - Path::new("assets").join(&assets).with_extension("squashfs"), - Entry::file(TmpSource::new( - tmp_dir.clone(), - PackSource::Squashfs(Arc::new(SqfsDir::new( - assets_dir.join(&assets), - tmp_dir.clone(), - ))), - )), - )?; - } + s9pk.as_archive_mut().contents_mut().insert_path( + "assets.squashfs", + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Squashfs(Arc::new(SqfsDir::new(assets_dir, tmp_dir.clone()))), + )), + )?; s9pk.load_images(tmp_dir.clone()).await?; @@ -816,9 +811,7 @@ pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result>, js_mount: MountGuard, volumes: BTreeMap, - assets: BTreeMap, + assets: Vec, pub(super) images: BTreeMap>, pub(super) subcontainers: Arc>>, pub(super) state: Arc>, @@ -168,35 +168,63 @@ impl PersistentContainer { .await?; volumes.insert(volume.clone(), mount); } - let mut assets = BTreeMap::new(); - for asset in &s9pk.as_manifest().assets { - let mountpoint = lxc_container - .rootfs_dir() - .join("media/startos/assets") - .join(asset); - tokio::fs::create_dir_all(&mountpoint).await?; - Command::new("chown") - .arg("100000:100000") - .arg(&mountpoint) - .invoke(crate::ErrorKind::Filesystem) - .await?; - let s9pk_asset_path = Path::new("assets").join(asset).with_extension("squashfs"); - let sqfs = s9pk - .as_archive() - .contents() - .get_path(&s9pk_asset_path) - .and_then(|e| e.as_file()) - .or_not_found(s9pk_asset_path.display())?; - assets.insert( - asset.clone(), + + let mountpoint = lxc_container.rootfs_dir().join("media/startos/assets"); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + let assets = if let Some(sqfs) = s9pk + .as_archive() + .contents() + .get_path("assets.squashfs") + .and_then(|e| e.as_file()) + { + vec![ MountGuard::mount( &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), mountpoint, MountType::ReadWrite, ) .await?, - ); - } + ] + } else if let Some(dir) = s9pk + .as_archive() + .contents() + .get_path("assets") + .and_then(|e| e.as_directory()) + { + // backwards compatibility for alpha s9pks - remove eventually + let mut assets = Vec::new(); + for (asset, entry) in &**dir { + let mountpoint = lxc_container + .rootfs_dir() + .join("media/startos/assets") + .join(Path::new(asset).with_extension("")); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + let Some(sqfs) = entry.as_file() else { + continue; + }; + assets.push( + MountGuard::mount( + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), + mountpoint, + MountType::ReadWrite, + ) + .await?, + ); + } + assets + } else { + Vec::new() + }; let mut images = BTreeMap::new(); let image_path = lxc_container.rootfs_dir().join("media/startos/images"); @@ -432,7 +460,7 @@ impl PersistentContainer { for (_, volume) in volumes { errs.handle(volume.unmount(true).await); } - for (_, assets) in assets { + for assets in assets { errs.handle(assets.unmount(true).await); } for (_, overlay) in std::mem::take(&mut *subcontainers.lock().await) { diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index c328f2689..4dd5d8cdb 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -35,6 +35,7 @@ mod v0_3_6_alpha_12; mod v0_3_6_alpha_13; mod v0_3_6_alpha_14; mod v0_3_6_alpha_15; +mod v0_3_6_alpha_16; mod v0_4_0_alpha_0; @@ -136,6 +137,7 @@ enum Version { V0_3_6_alpha_13(Wrapper), V0_3_6_alpha_14(Wrapper), V0_3_6_alpha_15(Wrapper), + V0_3_6_alpha_16(Wrapper), V0_4_0_alpha_0(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -176,6 +178,7 @@ impl Version { Self::V0_3_6_alpha_13(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_14(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_15(v) => DynVersion(Box::new(v.0)), + Self::V0_3_6_alpha_16(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_0(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( @@ -208,6 +211,7 @@ impl Version { Version::V0_3_6_alpha_13(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_14(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_15(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_16(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_0(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } diff --git a/core/startos/src/version/v0_3_6_alpha_16.rs b/core/startos/src/version/v0_3_6_alpha_16.rs new file mode 100644 index 000000000..84017de7e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_16.rs @@ -0,0 +1,36 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_15, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_16: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 16.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_15::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_3_6_alpha_16.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_4_0_alpha_0.rs b/core/startos/src/version/v0_4_0_alpha_0.rs index 460f6e2fe..ceb118780 100644 --- a/core/startos/src/version/v0_4_0_alpha_0.rs +++ b/core/startos/src/version/v0_4_0_alpha_0.rs @@ -2,7 +2,7 @@ use exver::{PreReleaseSegment, VersionRange}; use imbl_value::json; use super::v0_3_5::V0_3_0_COMPAT; -use super::{v0_3_6_alpha_15, VersionT}; +use super::{v0_3_6_alpha_16, VersionT}; use crate::prelude::*; lazy_static::lazy_static! { @@ -16,7 +16,7 @@ lazy_static::lazy_static! { pub struct Version; impl VersionT for Version { - type Previous = v0_3_6_alpha_15::Version; + type Previous = v0_3_6_alpha_16::Version; type PreUpRes = (); async fn pre_up(self) -> Result { diff --git a/sdk/Makefile b/sdk/Makefile index 3f8ae533a..8cd295656 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -58,11 +58,16 @@ check: fmt: package/node_modules base/node_modules npx prettier . "**/*.ts" --write +package/package-lock.json: package/package.json + cd package && npm i -package/node_modules: package/package.json +base/package-lock.json: base/package.json + cd base && npm i + +package/node_modules: package/package-lock.json cd package && npm ci -base/node_modules: base/package.json +base/node_modules: base/package-lock.json cd base && npm ci node_modules: package/node_modules base/node_modules diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index 135a2942c..b7c67af12 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -28,7 +28,7 @@ import { UrlString } from "./util/getServiceInterface" /** Used to reach out from the pure js runtime */ export type Effects = { - constRetry: () => void + constRetry?: () => void clearCallbacks: ( options: { only: number[] } | { except: number[] }, ) => Promise diff --git a/sdk/base/lib/exver/exver.pegjs b/sdk/base/lib/exver/exver.pegjs index fc11bb9b5..e9dbd9523 100644 --- a/sdk/base/lib/exver/exver.pegjs +++ b/sdk/base/lib/exver/exver.pegjs @@ -14,6 +14,7 @@ VersionRangeAtom / Not / Any / None + / FlavorAtom Parens = "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } } @@ -24,13 +25,16 @@ Anchor VersionSpec = flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } } +FlavorAtom + = "#" flavor:Lowercase { return { type: "Flavor", flavor: flavor } } + Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }} Any = "*" { return { type: "Any" } } None = "!" { return { type: "None" } } -CmpOp +CmpOp = ">=" { return ">="; } / "<=" { return "<="; } / ">" { return ">"; } @@ -89,7 +93,7 @@ String Version = number:VersionNumber prerelease: PreRelease? { - return { + return { number, prerelease: prerelease || [] }; @@ -106,7 +110,7 @@ PreReleaseSegment } VersionNumber - = first:Digit rest:("." Digit)* { + = first:Digit rest:("." Digit)* { return [first].concat(rest.map(r => r[1])); } @@ -114,4 +118,4 @@ Digit = [0-9]+ { return parseInt(text(), 10); } _ "whitespace" - = [ \t\n\r]* \ No newline at end of file + = [ \t\n\r]* diff --git a/sdk/base/lib/exver/exver.ts b/sdk/base/lib/exver/exver.ts index 60f766d69..9bb2a776a 100644 --- a/sdk/base/lib/exver/exver.ts +++ b/sdk/base/lib/exver/exver.ts @@ -296,7 +296,7 @@ function peg$parse(input, options) { var peg$source = options.grammarSource; // @ts-ignore - var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmverVersionRange: peg$parseEmverVersionRange, EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, EmverParens: peg$parseEmverParens, EmverAnchor: peg$parseEmverAnchor, EmverNot: peg$parseEmverNot, Emver: peg$parseEmver, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, FlavorAtom: peg$parseFlavorAtom, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmverVersionRange: peg$parseEmverVersionRange, EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, EmverParens: peg$parseEmverParens, EmverAnchor: peg$parseEmverAnchor, EmverNot: peg$parseEmverNot, Emver: peg$parseEmver, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; // @ts-ignore var peg$startRuleFunction = peg$parseVersionRange; @@ -306,18 +306,18 @@ function peg$parse(input, options) { var peg$c2 = "("; var peg$c3 = ")"; var peg$c4 = ":"; - var peg$c5 = "!"; - var peg$c6 = "*"; - var peg$c7 = ">="; - var peg$c8 = "<="; - var peg$c9 = ">"; - var peg$c10 = "<"; - var peg$c11 = "="; - var peg$c12 = "!="; - var peg$c13 = "^"; - var peg$c14 = "~"; - var peg$c15 = "."; - var peg$c16 = "#"; + var peg$c5 = "#"; + var peg$c6 = "!"; + var peg$c7 = "*"; + var peg$c8 = ">="; + var peg$c9 = "<="; + var peg$c10 = ">"; + var peg$c11 = "<"; + var peg$c12 = "="; + var peg$c13 = "!="; + var peg$c14 = "^"; + var peg$c15 = "~"; + var peg$c16 = "."; var peg$c17 = "-"; var peg$r0 = /^[a-z]/; @@ -330,18 +330,18 @@ function peg$parse(input, options) { var peg$e2 = peg$literalExpectation("(", false); var peg$e3 = peg$literalExpectation(")", false); var peg$e4 = peg$literalExpectation(":", false); - var peg$e5 = peg$literalExpectation("!", false); - var peg$e6 = peg$literalExpectation("*", false); - var peg$e7 = peg$literalExpectation(">=", false); - var peg$e8 = peg$literalExpectation("<=", false); - var peg$e9 = peg$literalExpectation(">", false); - var peg$e10 = peg$literalExpectation("<", false); - var peg$e11 = peg$literalExpectation("=", false); - var peg$e12 = peg$literalExpectation("!=", false); - var peg$e13 = peg$literalExpectation("^", false); - var peg$e14 = peg$literalExpectation("~", false); - var peg$e15 = peg$literalExpectation(".", false); - var peg$e16 = peg$literalExpectation("#", false); + var peg$e5 = peg$literalExpectation("#", false); + var peg$e6 = peg$literalExpectation("!", false); + var peg$e7 = peg$literalExpectation("*", false); + var peg$e8 = peg$literalExpectation(">=", false); + var peg$e9 = peg$literalExpectation("<=", false); + var peg$e10 = peg$literalExpectation(">", false); + var peg$e11 = peg$literalExpectation("<", false); + var peg$e12 = peg$literalExpectation("=", false); + var peg$e13 = peg$literalExpectation("!=", false); + var peg$e14 = peg$literalExpectation("^", false); + var peg$e15 = peg$literalExpectation("~", false); + var peg$e16 = peg$literalExpectation(".", false); var peg$e17 = peg$classExpectation([["a", "z"]], false, false); var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); var peg$e19 = peg$literalExpectation("-", false); @@ -359,57 +359,60 @@ function peg$parse(input, options) { var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore - var peg$f3 = function(value) {// @ts-ignore + var peg$f3 = function(flavor) {// @ts-ignore + return { type: "Flavor", flavor: flavor } };// @ts-ignore + + var peg$f4 = function(value) {// @ts-ignore return { type: "Not", value: value }};// @ts-ignore - var peg$f4 = function() {// @ts-ignore + var peg$f5 = function() {// @ts-ignore return { type: "Any" } };// @ts-ignore - var peg$f5 = function() {// @ts-ignore + var peg$f6 = function() {// @ts-ignore return { type: "None" } };// @ts-ignore - var peg$f6 = function() {// @ts-ignore + var peg$f7 = function() {// @ts-ignore return ">="; };// @ts-ignore - var peg$f7 = function() {// @ts-ignore + var peg$f8 = function() {// @ts-ignore return "<="; };// @ts-ignore - var peg$f8 = function() {// @ts-ignore + var peg$f9 = function() {// @ts-ignore return ">"; };// @ts-ignore - var peg$f9 = function() {// @ts-ignore + var peg$f10 = function() {// @ts-ignore return "<"; };// @ts-ignore - var peg$f10 = function() {// @ts-ignore + var peg$f11 = function() {// @ts-ignore return "="; };// @ts-ignore - var peg$f11 = function() {// @ts-ignore + var peg$f12 = function() {// @ts-ignore return "!="; };// @ts-ignore - var peg$f12 = function() {// @ts-ignore + var peg$f13 = function() {// @ts-ignore return "^"; };// @ts-ignore - var peg$f13 = function() {// @ts-ignore + var peg$f14 = function() {// @ts-ignore return "~"; };// @ts-ignore - var peg$f14 = function(flavor, upstream, downstream) { + var peg$f15 = function(flavor, upstream, downstream) { // @ts-ignore return { flavor: flavor || null, upstream, downstream } };// @ts-ignore - var peg$f15 = function(expr) {// @ts-ignore + var peg$f16 = function(expr) {// @ts-ignore return { type: "Parens", expr } };// @ts-ignore - var peg$f16 = function(operator, version) {// @ts-ignore + var peg$f17 = function(operator, version) {// @ts-ignore return { type: "Anchor", operator, version } };// @ts-ignore - var peg$f17 = function(value) {// @ts-ignore + var peg$f18 = function(value) {// @ts-ignore return { type: "Not", value: value }};// @ts-ignore - var peg$f18 = function(major, minor, patch, revision) {// @ts-ignore + var peg$f19 = function(major, minor, patch, revision) {// @ts-ignore return revision };// @ts-ignore - var peg$f19 = function(major, minor, patch, revision) { + var peg$f20 = function(major, minor, patch, revision) { // @ts-ignore return { // @ts-ignore @@ -431,18 +434,18 @@ function peg$parse(input, options) { } };// @ts-ignore - var peg$f20 = function(flavor) {// @ts-ignore + var peg$f21 = function(flavor) {// @ts-ignore return flavor };// @ts-ignore - var peg$f21 = function() {// @ts-ignore + var peg$f22 = function() {// @ts-ignore return text() };// @ts-ignore - var peg$f22 = function() {// @ts-ignore + var peg$f23 = function() {// @ts-ignore return text(); };// @ts-ignore - var peg$f23 = function(number, prerelease) { + var peg$f24 = function(number, prerelease) { // @ts-ignore - return { + return { // @ts-ignore number, // @ts-ignore @@ -450,22 +453,22 @@ function peg$parse(input, options) { }; };// @ts-ignore - var peg$f24 = function(first, rest) { + var peg$f25 = function(first, rest) { // @ts-ignore return [first].concat(rest.map(r => r[1])); };// @ts-ignore - var peg$f25 = function(segment) { + var peg$f26 = function(segment) { // @ts-ignore return segment; };// @ts-ignore - var peg$f26 = function(first, rest) { + var peg$f27 = function(first, rest) { // @ts-ignore return [first].concat(rest.map(r => r[1])); };// @ts-ignore - var peg$f27 = function() {// @ts-ignore + var peg$f28 = function() {// @ts-ignore return parseInt(text(), 10); }; // @ts-ignore var peg$currPos = 0; @@ -928,6 +931,11 @@ peg$parseVersionRangeAtom() { if (s0 === peg$FAILED) { // @ts-ignore s0 = peg$parseNone(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseFlavorAtom(); + } } } } @@ -1131,14 +1139,14 @@ peg$parseVersionSpec() { // @ts-ignore function // @ts-ignore -peg$parseNot() { +peg$parseFlavorAtom() { // @ts-ignore - var s0, s1, s2, s3; + var s0, s1, s2; // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.charCodeAt(peg$currPos) === 33) { + if (input.charCodeAt(peg$currPos) === 35) { // @ts-ignore s1 = peg$c5; // @ts-ignore @@ -1152,6 +1160,56 @@ peg$parseNot() { } // @ts-ignore if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseLowercase(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { // @ts-ignore s2 = peg$parse_(); // @ts-ignore @@ -1161,7 +1219,7 @@ peg$parseNot() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f3(s3); + s0 = peg$f4(s3); // @ts-ignore } else { // @ts-ignore @@ -1192,7 +1250,7 @@ peg$parseAny() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 42) { // @ts-ignore - s1 = peg$c6; + s1 = peg$c7; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1200,14 +1258,14 @@ peg$parseAny() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e6); } + if (peg$silentFails === 0) { peg$fail(peg$e7); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f4(); + s1 = peg$f5(); } // @ts-ignore s0 = s1; @@ -1227,7 +1285,7 @@ peg$parseNone() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 33) { // @ts-ignore - s1 = peg$c5; + s1 = peg$c6; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1235,14 +1293,14 @@ peg$parseNone() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f5(); + s1 = peg$f6(); } // @ts-ignore s0 = s1; @@ -1260,9 +1318,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c7) { + if (input.substr(peg$currPos, 2) === peg$c8) { // @ts-ignore - s1 = peg$c7; + s1 = peg$c8; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1270,14 +1328,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e7); } + if (peg$silentFails === 0) { peg$fail(peg$e8); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f6(); + s1 = peg$f7(); } // @ts-ignore s0 = s1; @@ -1286,9 +1344,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c8) { + if (input.substr(peg$currPos, 2) === peg$c9) { // @ts-ignore - s1 = peg$c8; + s1 = peg$c9; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1296,14 +1354,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e8); } + if (peg$silentFails === 0) { peg$fail(peg$e9); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f7(); + s1 = peg$f8(); } // @ts-ignore s0 = s1; @@ -1314,7 +1372,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 62) { // @ts-ignore - s1 = peg$c9; + s1 = peg$c10; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1322,14 +1380,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e9); } + if (peg$silentFails === 0) { peg$fail(peg$e10); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f8(); + s1 = peg$f9(); } // @ts-ignore s0 = s1; @@ -1340,7 +1398,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 60) { // @ts-ignore - s1 = peg$c10; + s1 = peg$c11; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1348,14 +1406,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e10); } + if (peg$silentFails === 0) { peg$fail(peg$e11); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f9(); + s1 = peg$f10(); } // @ts-ignore s0 = s1; @@ -1366,7 +1424,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 61) { // @ts-ignore - s1 = peg$c11; + s1 = peg$c12; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1374,14 +1432,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f10(); + s1 = peg$f11(); } // @ts-ignore s0 = s1; @@ -1390,9 +1448,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c12) { + if (input.substr(peg$currPos, 2) === peg$c13) { // @ts-ignore - s1 = peg$c12; + s1 = peg$c13; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1400,14 +1458,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e12); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f11(); + s1 = peg$f12(); } // @ts-ignore s0 = s1; @@ -1418,7 +1476,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 94) { // @ts-ignore - s1 = peg$c13; + s1 = peg$c14; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1426,14 +1484,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f12(); + s1 = peg$f13(); } // @ts-ignore s0 = s1; @@ -1444,7 +1502,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 126) { // @ts-ignore - s1 = peg$c14; + s1 = peg$c15; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1452,14 +1510,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e14); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f13(); + s1 = peg$f14(); } // @ts-ignore s0 = s1; @@ -1516,7 +1574,7 @@ peg$parseExtendedVersion() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f14(s1, s2, s4); + s0 = peg$f15(s1, s2, s4); // @ts-ignore } else { // @ts-ignore @@ -1756,7 +1814,7 @@ peg$parseEmverParens() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f15(s3); + s0 = peg$f16(s3); // @ts-ignore } else { // @ts-ignore @@ -1807,7 +1865,7 @@ peg$parseEmverAnchor() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f16(s1, s3); + s0 = peg$f17(s1, s3); // @ts-ignore } else { // @ts-ignore @@ -1831,7 +1889,7 @@ peg$parseEmverNot() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 33) { // @ts-ignore - s1 = peg$c5; + s1 = peg$c6; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1839,7 +1897,7 @@ peg$parseEmverNot() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } // @ts-ignore if (s1 !== peg$FAILED) { @@ -1852,7 +1910,7 @@ peg$parseEmverNot() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f17(s3); + s0 = peg$f18(s3); // @ts-ignore } else { // @ts-ignore @@ -1887,7 +1945,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s2 = peg$c15; + s2 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1895,7 +1953,7 @@ peg$parseEmver() { // @ts-ignore s2 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s2 !== peg$FAILED) { @@ -1906,7 +1964,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1914,7 +1972,7 @@ peg$parseEmver() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -1927,7 +1985,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s7 = peg$c15; + s7 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1935,7 +1993,7 @@ peg$parseEmver() { // @ts-ignore s7 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s7 !== peg$FAILED) { @@ -1946,7 +2004,7 @@ peg$parseEmver() { // @ts-ignore peg$savedPos = s6; // @ts-ignore - s6 = peg$f18(s1, s3, s5, s8); + s6 = peg$f19(s1, s3, s5, s8); // @ts-ignore } else { // @ts-ignore @@ -1969,7 +2027,7 @@ peg$parseEmver() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f19(s1, s3, s5, s6); + s0 = peg$f20(s1, s3, s5, s6); // @ts-ignore } else { // @ts-ignore @@ -2021,7 +2079,7 @@ peg$parseFlavor() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 35) { // @ts-ignore - s1 = peg$c16; + s1 = peg$c5; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2029,7 +2087,7 @@ peg$parseFlavor() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e5); } } // @ts-ignore if (s1 !== peg$FAILED) { @@ -2055,7 +2113,7 @@ peg$parseFlavor() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f20(s2); + s0 = peg$f21(s2); // @ts-ignore } else { // @ts-ignore @@ -2135,7 +2193,7 @@ peg$parseLowercase() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f21(); + s1 = peg$f22(); } // @ts-ignore s0 = s1; @@ -2197,7 +2255,7 @@ peg$parseString() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f22(); + s1 = peg$f23(); } // @ts-ignore s0 = s1; @@ -2228,7 +2286,7 @@ peg$parseVersion() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f23(s1, s2); + s0 = peg$f24(s1, s2); // @ts-ignore } else { // @ts-ignore @@ -2275,7 +2333,7 @@ peg$parsePreRelease() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s5 = peg$c15; + s5 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2283,7 +2341,7 @@ peg$parsePreRelease() { // @ts-ignore s5 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s5 !== peg$FAILED) { @@ -2318,7 +2376,7 @@ peg$parsePreRelease() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s5 = peg$c15; + s5 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2326,7 +2384,7 @@ peg$parsePreRelease() { // @ts-ignore s5 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s5 !== peg$FAILED) { @@ -2356,7 +2414,7 @@ peg$parsePreRelease() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f24(s2, s3); + s0 = peg$f25(s2, s3); // @ts-ignore } else { // @ts-ignore @@ -2387,7 +2445,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s1 = peg$c15; + s1 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2395,7 +2453,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s1 === peg$FAILED) { @@ -2414,7 +2472,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f25(s2); + s0 = peg$f26(s2); // @ts-ignore } else { // @ts-ignore @@ -2446,7 +2504,7 @@ peg$parseVersionNumber() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2454,7 +2512,7 @@ peg$parseVersionNumber() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -2489,7 +2547,7 @@ peg$parseVersionNumber() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2497,7 +2555,7 @@ peg$parseVersionNumber() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -2527,7 +2585,7 @@ peg$parseVersionNumber() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f26(s1, s2); + s0 = peg$f27(s1, s2); // @ts-ignore } else { // @ts-ignore @@ -2593,7 +2651,7 @@ peg$parseDigit() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f27(); + s1 = peg$f28(); } // @ts-ignore s0 = s1; @@ -2764,7 +2822,7 @@ peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; export interface ParseOptions { filename?: string; - startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmverVersionRange" | "EmverVersionRangeAtom" | "EmverParens" | "EmverAnchor" | "EmverNot" | "Emver" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "FlavorAtom" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmverVersionRange" | "EmverVersionRangeAtom" | "EmverParens" | "EmverAnchor" | "EmverNot" | "Emver" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; tracer?: any; [key: string]: any; } @@ -2779,6 +2837,7 @@ export type ParseFunction = ( StartRule extends "Parens" ? Parens : StartRule extends "Anchor" ? Anchor : StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "FlavorAtom" ? FlavorAtom : StartRule extends "Not" ? Not : StartRule extends "Any" ? Any : StartRule extends "None" ? None : @@ -2813,7 +2872,7 @@ export type VersionRange = [ ]; export type Or = "||"; export type And = "&&"; -export type VersionRangeAtom = Parens | Anchor | Not | Any | None; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None | FlavorAtom; export type Parens = { type: "Parens"; expr: VersionRange }; export type Anchor = { type: "Anchor"; @@ -2825,6 +2884,7 @@ export type VersionSpec = { upstream: Version; downstream: any; }; +export type FlavorAtom = { type: "Flavor"; flavor: Lowercase_1 }; export type Not = { type: "Not"; value: VersionRangeAtom }; export type Any = { type: "Any" }; export type None = { type: "None" }; diff --git a/sdk/base/lib/exver/index.ts b/sdk/base/lib/exver/index.ts index 9965d7287..c855e1534 100644 --- a/sdk/base/lib/exver/index.ts +++ b/sdk/base/lib/exver/index.ts @@ -1,7 +1,8 @@ +import { DeepMap } from "deep-equality-data-structures" import * as P from "./exver" // prettier-ignore -export type ValidateVersion = +export type ValidateVersion = T extends `-${infer A}` ? never : T extends `${infer A}-${string}` ? ValidateVersion : T extends `${bigint}` ? unknown : @@ -9,9 +10,9 @@ T extends `${infer A}-${string}` ? ValidateVersion : never // prettier-ignore -export type ValidateExVer = +export type ValidateExVer = T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : - T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : never // prettier-ignore @@ -43,19 +44,462 @@ type Not = { value: VersionRange } +type Flavor = { + type: "Flavor" + flavor: string | null +} + +type FlavorNot = { + type: "FlavorNot" + flavors: Set +} + +type FlavorAtom = Flavor | FlavorNot + +/** + * Splits a number line of versions in half, so that every possible semver is either to the left or right. + * The `side` field handles inclusively. + * + * # Example + * Consider the version `1.2.3`. For side=-1 the version point is like `1.2.2.999*.999*.**` (that is, 1.2.3.0.0.** is greater) and + * for side=+1 the point is like `1.2.3.0.0.**.1` (that is, 1.2.3.0.0.** is less). + */ +type VersionRangePoint = { + upstream: Version + downstream: Version + side: -1 | 1 +} + +function compareVersionRangePoints( + a: VersionRangePoint, + b: VersionRangePoint, +): -1 | 0 | 1 { + let up = a.upstream.compareForSort(b.upstream) + if (up != 0) { + return up + } + let down = a.upstream.compareForSort(b.upstream) + if (down != 0) { + return down + } + if (a.side < b.side) { + return -1 + } else if (a.side > b.side) { + return 1 + } else { + return 0 + } +} + +function adjacentVersionRangePoints( + a: VersionRangePoint, + b: VersionRangePoint, +): boolean { + let up = a.upstream.compareForSort(b.upstream) + if (up != 0) { + return false + } + let down = a.upstream.compareForSort(b.upstream) + if (down != 0) { + return false + } + return a.side == -1 && b.side == 1 +} + +function flavorAnd(a: FlavorAtom, b: FlavorAtom): FlavorAtom | null { + if (a.type == "Flavor") { + if (b.type == "Flavor") { + if (a.flavor == b.flavor) { + return a + } else { + return null + } + } else { + if (b.flavors.has(a.flavor)) { + return null + } else { + return a + } + } + } else { + if (b.type == "Flavor") { + if (a.flavors.has(b.flavor)) { + return null + } else { + return b + } + } else { + // TODO: use Set.union if targeting esnext or later + return { + type: "FlavorNot", + flavors: new Set([...a.flavors, ...b.flavors]), + } + } + } +} + +/** + * Truth tables for version numbers and flavors. For each flavor we need a separate table, which + * is quite straightforward. But in order to exhaustively enumerate the boolean values of every + * combination of flavors and versions we also need tables for flavor negations. + */ +type VersionRangeTables = DeepMap | boolean + +/** + * A truth table for version numbers. This is easiest to picture as a number line, cut up into + * ranges of versions between version points. + */ +class VersionRangeTable { + private constructor( + protected points: Array, + protected values: boolean[], + ) {} + + static zip( + a: VersionRangeTable, + b: VersionRangeTable, + func: (a: boolean, b: boolean) => boolean, + ): VersionRangeTable { + let c = new VersionRangeTable([], []) + let i = 0 + let j = 0 + while (true) { + let next = func(a.values[i], b.values[j]) + if (c.values.length > 0 && c.values[c.values.length - 1] == next) { + // collapse automatically + c.points.pop() + } else { + c.values.push(next) + } + + // which point do we step over? + if (i == a.points.length) { + if (j == b.points.length) { + // just added the last segment, no point to jump over + return c + } else { + // i has reach the end, step over j + c.points.push(b.points[j]) + j += 1 + } + } else { + if (j == b.points.length) { + // j has reached the end, step over i + c.points.push(a.points[i]) + i += 1 + } else { + // depends on which of the next two points is lower + switch (compareVersionRangePoints(a.points[i], b.points[j])) { + case -1: + // i is the lower point + c.points.push(a.points[i]) + i += 1 + break + case 1: + // j is the lower point + c.points.push(b.points[j]) + j += 1 + break + default: + // step over both + c.points.push(a.points[i]) + i += 1 + j += 1 + break + } + } + } + } + } + + /** + * Creates a version table which is `true` for the given flavor, and `false` for any other flavor. + */ + static eqFlavor(flavor: string | null): VersionRangeTables { + return new DeepMap([ + [ + { type: "Flavor", flavor } as FlavorAtom, + new VersionRangeTable([], [true]), + ], + // make sure the truth table is exhaustive, or `not` will not work properly. + [ + { type: "FlavorNot", flavors: new Set([flavor]) } as FlavorAtom, + new VersionRangeTable([], [false]), + ], + ]) + } + + /** + * Creates a version table with exactly two ranges (to the left and right of the given point) and with `false` for any other flavor. + * This is easiest to understand by looking at `VersionRange.tables`. + */ + static cmpPoint( + flavor: string | null, + point: VersionRangePoint, + left: boolean, + right: boolean, + ): VersionRangeTables { + return new DeepMap([ + [ + { type: "Flavor", flavor } as FlavorAtom, + new VersionRangeTable([point], [left, right]), + ], + // make sure the truth table is exhaustive, or `not` will not work properly. + [ + { type: "FlavorNot", flavors: new Set([flavor]) } as FlavorAtom, + new VersionRangeTable([], [false]), + ], + ]) + } + + /** + * Helper for `cmpPoint`. + */ + static cmp( + version: ExtendedVersion, + side: -1 | 1, + left: boolean, + right: boolean, + ): VersionRangeTables { + return VersionRangeTable.cmpPoint( + version.flavor, + { upstream: version.upstream, downstream: version.downstream, side }, + left, + right, + ) + } + + static not(tables: VersionRangeTables) { + if (tables === true || tables === false) { + return !tables + } + // because tables are always exhaustive, we can simply invert each range + for (let [f, t] of tables) { + for (let i = 0; i < t.values.length; i++) { + t.values[i] = !t.values[i] + } + } + return tables + } + + static and( + a_tables: VersionRangeTables, + b_tables: VersionRangeTables, + ): VersionRangeTables { + if (a_tables === true) { + return b_tables + } + if (b_tables === true) { + return a_tables + } + if (a_tables === false || b_tables == false) { + return false + } + let c_tables: VersionRangeTables = true + for (let [f_a, a] of a_tables) { + for (let [f_b, b] of b_tables) { + let flavor = flavorAnd(f_a, f_b) + if (flavor == null) { + continue + } + let c = VersionRangeTable.zip(a, b, (a, b) => a && b) + if (c_tables === true) { + c_tables = new DeepMap() + } + let prev_c = c_tables.get(flavor) + if (prev_c == null) { + c_tables.set(flavor, c) + } else { + c_tables.set( + flavor, + VersionRangeTable.zip(c, prev_c, (a, b) => a || b), + ) + } + } + } + return c_tables + } + + static or(...in_tables: VersionRangeTables[]): VersionRangeTables { + let out_tables: VersionRangeTables = false + for (let tables of in_tables) { + if (tables === false) { + continue + } + if (tables === true) { + return true + } + if (out_tables === false) { + out_tables = new DeepMap() + } + for (let [flavor, table] of tables) { + let prev = out_tables.get(flavor) + if (prev == null) { + out_tables.set(flavor, table) + } else { + out_tables.set( + flavor, + VersionRangeTable.zip(table, prev, (a, b) => a || b), + ) + } + } + } + return out_tables + } + + /** + * If this is true for all versions or false for all versions, returen that value. Otherwise return null. + */ + static collapse(tables: VersionRangeTables): boolean | null { + if (tables === true || tables === false) { + return tables + } else { + let found = null + for (let table of tables.values()) { + for (let x of table.values) { + if (found == null) { + found = x + } else if (found != x) { + return null + } + } + } + return found + } + } + + /** + * Expresses this truth table as a series of version range operators. + * https://en.wikipedia.org/wiki/Canonical_normal_form#Minterms + */ + static minterms(tables: VersionRangeTables): VersionRange { + let collapse = VersionRangeTable.collapse(tables) + if (tables === true || collapse === true) { + return VersionRange.any() + } + if (tables == false || collapse === false) { + return VersionRange.none() + } + let sum_terms: VersionRange[] = [] + for (let [flavor, table] of tables) { + let cmp_flavor = null + if (flavor.type == "Flavor") { + cmp_flavor = flavor.flavor + } + for (let i = 0; i < table.values.length; i++) { + let term: VersionRange[] = [] + if (!table.values[i]) { + continue + } + + if (flavor.type == "FlavorNot") { + for (let not_flavor of flavor.flavors) { + term.push(VersionRange.flavor(not_flavor).not()) + } + } + + let p = null + let q = null + if (i > 0) { + p = table.points[i - 1] + } + if (i < table.points.length) { + q = table.points[i] + } + + if (p != null && q != null && adjacentVersionRangePoints(p, q)) { + term.push( + VersionRange.anchor( + "=", + new ExtendedVersion(cmp_flavor, p.upstream, p.downstream), + ), + ) + } else { + if (p != null && p.side < 0) { + term.push( + VersionRange.anchor( + ">=", + new ExtendedVersion(cmp_flavor, p.upstream, p.downstream), + ), + ) + } + if (p != null && p.side >= 0) { + term.push( + VersionRange.anchor( + ">", + new ExtendedVersion(cmp_flavor, p.upstream, p.downstream), + ), + ) + } + if (q != null && q.side < 0) { + term.push( + VersionRange.anchor( + "<", + new ExtendedVersion(cmp_flavor, q.upstream, q.downstream), + ), + ) + } + if (q != null && q.side >= 0) { + term.push( + VersionRange.anchor( + "<=", + new ExtendedVersion(cmp_flavor, q.upstream, q.downstream), + ), + ) + } + } + + if (term.length == 0) { + term.push(VersionRange.flavor(cmp_flavor)) + } + + sum_terms.push(VersionRange.and(...term)) + } + } + return VersionRange.or(...sum_terms) + } +} + export class VersionRange { - private constructor(public atom: Anchor | And | Or | Not | P.Any | P.None) {} + constructor(public atom: Anchor | And | Or | Not | P.Any | P.None | Flavor) {} + + toStringParens(parent: "And" | "Or" | "Not") { + let needs = true + switch (this.atom.type) { + case "And": + case "Or": + needs = parent != this.atom.type + break + case "Anchor": + case "Any": + case "None": + needs = parent == "Not" + break + case "Not": + case "Flavor": + needs = false + break + } + + if (needs) { + return "(" + this.toString() + ")" + } else { + return this.toString() + } + } 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()})` + return `${this.atom.left.toStringParens(this.atom.type)} && ${this.atom.right.toStringParens(this.atom.type)}` case "Or": - return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})` + return `${this.atom.left.toStringParens(this.atom.type)} || ${this.atom.right.toStringParens(this.atom.type)}` case "Not": - return `!(${this.atom.value.toString()})` + return `!${this.atom.value.toStringParens(this.atom.type)}` + case "Flavor": + return this.atom.flavor == null ? `#` : `#${this.atom.flavor}` case "Any": return "*" case "None": @@ -88,6 +532,8 @@ export class VersionRange { ), ), }) + case "Flavor": + return VersionRange.flavor(atom.flavor) default: return new VersionRange(atom) } @@ -123,6 +569,14 @@ export class VersionRange { ) } + static anchor(operator: P.CmpOp, version: ExtendedVersion) { + return new VersionRange({ type: "Anchor", operator, version }) + } + + static flavor(flavor: string | null) { + return new VersionRange({ type: "Flavor", flavor }) + } + static parseEmver(range: string): VersionRange { return VersionRange.parseRange( P.parse(range, { startRule: "EmverVersionRange" }), @@ -141,8 +595,40 @@ export class VersionRange { return new VersionRange({ type: "Not", value: this }) } - static anchor(operator: P.CmpOp, version: ExtendedVersion) { - return new VersionRange({ type: "Anchor", operator, version }) + static and(...xs: Array) { + let y = VersionRange.any() + for (let x of xs) { + if (x.atom.type == "Any") { + continue + } + if (x.atom.type == "None") { + return x + } + if (y.atom.type == "Any") { + y = x + } else { + y = new VersionRange({ type: "And", left: y, right: x }) + } + } + return y + } + + static or(...xs: Array) { + let y = VersionRange.none() + for (let x of xs) { + if (x.atom.type == "None") { + continue + } + if (x.atom.type == "Any") { + return x + } + if (y.atom.type == "None") { + y = x + } else { + y = new VersionRange({ type: "Or", left: y, right: x }) + } + } + return y } static any() { @@ -156,6 +642,89 @@ export class VersionRange { satisfiedBy(version: Version | ExtendedVersion) { return version.satisfies(this) } + + tables(): VersionRangeTables { + switch (this.atom.type) { + case "Anchor": + switch (this.atom.operator) { + case "=": + // `=1.2.3` is equivalent to `>=1.2.3 && <=1.2.4 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version, 1, true, false), + ) + case ">": + return VersionRangeTable.cmp(this.atom.version, 1, false, true) + case "<": + return VersionRangeTable.cmp(this.atom.version, -1, true, false) + case ">=": + return VersionRangeTable.cmp(this.atom.version, -1, false, true) + case "<=": + return VersionRangeTable.cmp(this.atom.version, 1, true, false) + case "!=": + // `!=1.2.3` is equivalent to `!(>=1.2.3 && <=1.2.3 && #flavor)` + // **not** equivalent to `(<1.2.3 || >1.2.3) && #flavor` + return VersionRangeTable.not( + VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version, 1, true, false), + ), + ) + case "^": + // `^1.2.3` is equivalent to `>=1.2.3 && <2.0.0 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp( + this.atom.version.incrementMajor(), + -1, + true, + false, + ), + ) + case "~": + // `~1.2.3` is equivalent to `>=1.2.3 && <1.3.0 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp( + this.atom.version.incrementMinor(), + -1, + true, + false, + ), + ) + } + case "Flavor": + return VersionRangeTable.eqFlavor(this.atom.flavor) + case "Not": + return VersionRangeTable.not(this.atom.value.tables()) + case "And": + return VersionRangeTable.and( + this.atom.left.tables(), + this.atom.right.tables(), + ) + case "Or": + return VersionRangeTable.or( + this.atom.left.tables(), + this.atom.right.tables(), + ) + case "Any": + return true + case "None": + return false + } + } + + satisfiable(): boolean { + return VersionRangeTable.collapse(this.tables()) !== false + } + + intersects(other: VersionRange): boolean { + return VersionRange.and(this, other).satisfiable() + } + + normalize(): VersionRange { + return VersionRangeTable.minterms(this.tables()) + } } export class Version { @@ -211,6 +780,17 @@ export class Version { return "equal" } + compareForSort(other: Version): -1 | 0 | 1 { + switch (this.compare(other)) { + case "greater": + return 1 + case "equal": + return 0 + case "less": + return -1 + } + } + static parse(version: string): Version { const parsed = P.parse(version, { startRule: "Version" }) return new Version(parsed.number, parsed.prerelease) @@ -409,6 +989,8 @@ export class ExtendedVersion { return false } } + case "Flavor": + return versionRange.atom.flavor == this.flavor case "And": return ( this.satisfies(versionRange.atom.left) && @@ -433,6 +1015,7 @@ export const testTypeExVer = (t: T & ValidateExVer) => t export const testTypeVersion = (t: T & ValidateVersion) => t + function tests() { testTypeVersion("1.2.3") testTypeVersion("1") diff --git a/sdk/base/lib/osBindings/Manifest.ts b/sdk/base/lib/osBindings/Manifest.ts index 2c9a2457e..f65daeea8 100644 --- a/sdk/base/lib/osBindings/Manifest.ts +++ b/sdk/base/lib/osBindings/Manifest.ts @@ -26,7 +26,6 @@ export type Manifest = { donationUrl: string | null description: Description images: { [key: ImageId]: ImageConfig } - assets: Array volumes: Array alerts: Alerts dependencies: Dependencies diff --git a/sdk/base/lib/test/exverList.test.ts b/sdk/base/lib/test/exver.test.ts similarity index 82% rename from sdk/base/lib/test/exverList.test.ts rename to sdk/base/lib/test/exver.test.ts index e29a9f0d1..776796966 100644 --- a/sdk/base/lib/test/exverList.test.ts +++ b/sdk/base/lib/test/exver.test.ts @@ -70,6 +70,24 @@ describe("ExVer", () => { ) }) } + { + // TODO: this this correct? if not, also fix normalize + const checker = VersionRange.parse("=1") + test(`VersionRange.parse("=1") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.0:0"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse("=1") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.0:1"))).toEqual( + false, + ) + }) + } { const checker = VersionRange.parse(">=1.2.3:4") test(`VersionRange.parse(">=1.2.3:4") valid`, () => { @@ -290,6 +308,50 @@ describe("ExVer", () => { }) } + { + function testNormalization(input: string, expected: string) { + test(`"${input}" normalizes to "${expected}"`, () => { + const checker = VersionRange.parse(input).normalize() + expect(checker.toString()).toEqual(expected) + }) + } + + testNormalization("=2.0", "=2.0:0") + testNormalization("=1 && =2", "!") + testNormalization("!(=1 && =2)", "*") + testNormalization("!=1 || !=2", "*") + testNormalization("(!=#foo:1 || !=#foo:2) && #foo", "#foo") + testNormalization( + "!=#foo:1 || !=#bar:2", + "<#foo:1:0 || >#foo:1:0 || !#foo || <#bar:2:0 || >#bar:2:0 || !#bar", + ) + testNormalization("!(=1 || =2)", "<1:0 || (>1:0 && <2:0) || >2:0 || !#") + testNormalization("=1 && (=2 || =3)", "!") + testNormalization("=1 && (=1 || =2)", "=1:0") + testNormalization("=#foo:1 && =#bar:1", "!") + testNormalization( + "!(=#foo:1) && !(=#bar:1)", + "<#foo:1:0 || >#foo:1:0 || <#bar:1:0 || >#bar:1:0 || (!#foo && !#bar)", + ) + testNormalization("!(=#foo:1) && !(=#bar:1) && >2", ">2:0") + testNormalization("~1.2.3", ">=1.2.3:0 && <1.3.0:0") + testNormalization("^1.2.3", ">=1.2.3:0 && <2.0.0:0") + testNormalization( + "^1.2.3 && >=1 && >=1.2 && >=1.3", + ">=1.3:0 && <2.0.0:0", + ) + testNormalization( + "(>=1.0 && <1.1) || (>=1.1 && <1.2) || (>=1.2 && <1.3)", + ">=1.0:0 && <1.3:0", + ) + testNormalization(">1 || <2", "#") + + testNormalization("=1 && =1.2 && =1.2.3", "!") + // testNormalization("=1 && =1.2 && =1.2.3", "=1.2.3:0"); TODO: should it be this instead? + testNormalization("=1 || =1.2 || =1.2.3", "=1:0 || =1.2:0 || =1.2.3:0") + // testNormalization("=1 || =1.2 || =1.2.3", "=1:0"); TODO: should it be this instead? + } + { test(">1 && =1.2", () => { const checker = VersionRange.parse(">1 && =1.2") @@ -305,6 +367,12 @@ describe("ExVer", () => { const checker = VersionRange.parse("=1 || =2") expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) // really? + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) // really? expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) }) diff --git a/sdk/base/lib/test/startosTypeValidation.test.ts b/sdk/base/lib/test/startosTypeValidation.test.ts index dfccb6506..f6656e55d 100644 --- a/sdk/base/lib/test/startosTypeValidation.test.ts +++ b/sdk/base/lib/test/startosTypeValidation.test.ts @@ -45,7 +45,6 @@ type EffectsTypeChecker = { describe("startosTypeValidation ", () => { test(`checking the params match`, () => { typeEquality({ - constRetry: {}, clearCallbacks: {} as ClearCallbacksParams, action: { clear: {} as ClearActionsParams, diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index 056f986c2..140655178 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -20,10 +20,6 @@ export { } from "./dependencies/setupDependencies" export type ExposedStorePaths = string[] & Affine<"ExposedStorePaths"> -declare const HealthProof: unique symbol -export type HealthReceipt = { - [HealthProof]: never -} export type DaemonBuildable = { build(): Promise<{ diff --git a/sdk/base/lib/types/ManifestTypes.ts b/sdk/base/lib/types/ManifestTypes.ts index 76ab921b9..e7b0db48d 100644 --- a/sdk/base/lib/types/ManifestTypes.ts +++ b/sdk/base/lib/types/ManifestTypes.ts @@ -84,14 +84,6 @@ export type SDKManifest = { * ``` */ readonly images: Record - /** - * @description A list of readonly asset directories that will mount to the container. Each item here must - * correspond to a directory in the /assets directory of this project. - * - * Most projects will not make use of this. - * @example [] - */ - readonly assets: string[] /** * @description A list of data volumes that will mount to the container. Must contain at least one volume. * @example ['main'] diff --git a/sdk/base/lib/util/GetSystemSmtp.ts b/sdk/base/lib/util/GetSystemSmtp.ts index 8e7db00b6..2e560f509 100644 --- a/sdk/base/lib/util/GetSystemSmtp.ts +++ b/sdk/base/lib/util/GetSystemSmtp.ts @@ -9,7 +9,9 @@ export class GetSystemSmtp { */ const() { return this.effects.getSystemSmtp({ - callback: () => this.effects.constRetry(), + callback: + this.effects.constRetry && + (() => this.effects.constRetry && this.effects.constRetry()), }) } /** diff --git a/sdk/base/lib/util/deepMerge.ts b/sdk/base/lib/util/deepMerge.ts index 72392a887..055b3085f 100644 --- a/sdk/base/lib/util/deepMerge.ts +++ b/sdk/base/lib/util/deepMerge.ts @@ -24,10 +24,7 @@ export function partialDiff( return } } else if (typeof prev === "object" && typeof next === "object") { - if (prev === null) { - return { diff: next } - } - if (next === null) return + if (prev === null || next === null) return { diff: next } const res = { diff: {} as Record } for (let key in next) { const diff = partialDiff(prev[key], next[key]) diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index bb57b6c3b..58698a5f9 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -217,7 +217,9 @@ export class GetServiceInterface { */ async const() { const { id, packageId } = this.opts - const callback = () => this.effects.constRetry() + const callback = + this.effects.constRetry && + (() => this.effects.constRetry && this.effects.constRetry()) const interfaceFilled = await makeInterfaceFilled({ effects: this.effects, id, diff --git a/sdk/base/lib/util/getServiceInterfaces.ts b/sdk/base/lib/util/getServiceInterfaces.ts index de5a8d015..afb87c6d0 100644 --- a/sdk/base/lib/util/getServiceInterfaces.ts +++ b/sdk/base/lib/util/getServiceInterfaces.ts @@ -51,7 +51,9 @@ export class GetServiceInterfaces { */ async const() { const { packageId } = this.opts - const callback = () => this.effects.constRetry() + const callback = + this.effects.constRetry && + (() => this.effects.constRetry && this.effects.constRetry()) const interfaceFilled: ServiceInterfaceFilled[] = await makeManyInterfaceFilled({ effects: this.effects, diff --git a/sdk/base/package-lock.json b/sdk/base/package-lock.json index dc807bee0..533fd0db3 100644 --- a/sdk/base/package-lock.json +++ b/sdk/base/package-lock.json @@ -10,6 +10,7 @@ "@iarna/toml": "^2.2.5", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", + "deep-equality-data-structures": "^1.5.0", "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", @@ -2230,6 +2231,15 @@ } } }, + "node_modules/deep-equality-data-structures": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-1.5.1.tgz", + "integrity": "sha512-P7zsL2/AbZIGHDxbo/LLEhCp11AttRp8GvzXOXudqMT/qiGCLo/pyI4lAZvjUZyQnlIbPna3fv8DMsuRvLt4ww==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3910,6 +3920,15 @@ "node": ">=8" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/sdk/base/package.json b/sdk/base/package.json index 7a162c218..6a050b61b 100644 --- a/sdk/base/package.json +++ b/sdk/base/package.json @@ -28,7 +28,8 @@ "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", "ts-matches": "^6.2.1", - "yaml": "^2.2.2" + "yaml": "^2.2.2", + "deep-equality-data-structures": "^1.5.0" }, "prettier": { "trailingComma": "all", diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 40777d9c0..924455305 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -19,7 +19,6 @@ import { SyncOptions, ServiceInterfaceId, PackageId, - HealthReceipt, ServiceInterfaceType, Effects, } from "../../base/lib/types" @@ -27,7 +26,7 @@ import * as patterns from "../../base/lib/util/patterns" import { BackupSync, Backups } from "./backup/Backups" import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants" import { CommandController, Daemons } from "./mainFn/Daemons" -import { healthCheck, HealthCheckParams } from "./health/HealthCheck" +import { HealthCheck } from "./health/HealthCheck" import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkWebUrl, runHealthScript } from "./health/checkFns" import { List } from "../../base/lib/actions/input/builder/list" @@ -231,7 +230,7 @@ export class StartSdk { }, command: T.CommandType, options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] + mounts?: { mountpoint: string; options: MountOptions }[] }, /** * A name to use to refer to the ephemeral subcontainer for debugging purposes @@ -420,11 +419,7 @@ export class StartSdk { hostnames: string[], algorithm?: T.Algorithm, ) => new GetSslCertificate(effects, hostnames, algorithm), - HealthCheck: { - of(effects: T.Effects, o: Omit) { - return healthCheck({ effects, ...o }) - }, - }, + HealthCheck, healthCheck: { checkPortListening, checkWebUrl, @@ -677,9 +672,9 @@ export class StartSdk { of( effects: Effects, started: (onTerm: () => PromiseLike) => PromiseLike, - healthReceipts: HealthReceipt[], + healthChecks: HealthCheck[], ) { - return Daemons.of({ effects, started, healthReceipts }) + return Daemons.of({ effects, started, healthChecks }) }, }, SubContainer: { @@ -699,7 +694,7 @@ export class StartSdk { imageId: T.ImageId & keyof Manifest["images"] sharedRun?: boolean }, - mounts: { options: MountOptions; path: string }[], + mounts: { options: MountOptions; mountpoint: string }[], name: string, fn: (subContainer: SubContainer) => Promise, ): Promise { @@ -1081,7 +1076,7 @@ export async function runCommand( image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: T.CommandType, options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] + mounts?: { mountpoint: string; options: MountOptions }[] }, name?: string, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { diff --git a/sdk/package/lib/health/HealthCheck.ts b/sdk/package/lib/health/HealthCheck.ts index d921100a0..07a9179a5 100644 --- a/sdk/package/lib/health/HealthCheck.ts +++ b/sdk/package/lib/health/HealthCheck.ts @@ -1,13 +1,12 @@ -import { Effects, HealthCheckId, HealthReceipt } from "../../../base/lib/types" +import { Effects, HealthCheckId } from "../../../base/lib/types" import { HealthCheckResult } from "./checkFns/HealthCheckResult" import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" -import { once, asError } from "../util" +import { once, asError, Drop } from "../util" import { object, unknown } from "ts-matches" export type HealthCheckParams = { - effects: Effects id: HealthCheckId name: string trigger?: Trigger @@ -16,53 +15,110 @@ export type HealthCheckParams = { onFirstSuccess?: () => unknown | Promise } -export function healthCheck(o: HealthCheckParams) { - new Promise(async () => { - const start = performance.now() - let currentValue: TriggerInput = {} - const getCurrentValue = () => currentValue - const gracePeriod = o.gracePeriod ?? 5000 - const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) - const triggerFirstSuccess = once(() => - Promise.resolve( - "onFirstSuccess" in o && o.onFirstSuccess - ? o.onFirstSuccess() - : undefined, - ), - ) - for ( - let res = await trigger.next(); - !res.done; - res = await trigger.next() - ) { - try { - let { result, message } = await o.fn() - if (result === "failure" && performance.now() - start <= gracePeriod) - result = "starting" - await o.effects.setHealth({ - name: o.name, - id: o.id, - result, - message: message || "", - }) - currentValue.lastResult = result - await triggerFirstSuccess().catch((err) => { - console.error(asError(err)) - }) - } catch (e) { - await o.effects.setHealth({ - name: o.name, - id: o.id, - result: - performance.now() - start <= gracePeriod ? "starting" : "failure", - message: asMessage(e) || "", - }) - currentValue.lastResult = "failure" +export class HealthCheck extends Drop { + private started: number | null = null + private setStarted = (started: number | null) => { + this.started = started + } + private exited = false + private exit = () => { + this.exited = true + } + private currentValue: TriggerInput = {} + private promise: Promise + private constructor(effects: Effects, o: HealthCheckParams) { + super() + this.promise = Promise.resolve().then(async () => { + const getCurrentValue = () => this.currentValue + const gracePeriod = o.gracePeriod ?? 5000 + const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) + const triggerFirstSuccess = once(() => + Promise.resolve( + "onFirstSuccess" in o && o.onFirstSuccess + ? o.onFirstSuccess() + : undefined, + ), + ) + const checkStarted = () => + [ + this.started, + new Promise((resolve) => { + this.setStarted = (started: number | null) => { + this.started = started + resolve() + } + this.exit = () => { + this.exited = true + resolve() + } + }), + ] as const + let triggered = false + while (!this.exited) { + const [started, changed] = checkStarted() + let race: + | [Promise] + | [Promise, Promise>] = [ + changed, + ] + if (started) { + race = [...race, trigger.next()] + if (triggered) { + try { + let { result, message } = await o.fn() + if ( + result === "failure" && + performance.now() - started <= gracePeriod + ) + result = "starting" + await effects.setHealth({ + name: o.name, + id: o.id, + result, + message: message || "", + }) + this.currentValue.lastResult = result + await triggerFirstSuccess().catch((err) => { + console.error(asError(err)) + }) + } catch (e) { + await effects.setHealth({ + name: o.name, + id: o.id, + result: + performance.now() - started <= gracePeriod + ? "starting" + : "failure", + message: asMessage(e) || "", + }) + this.currentValue.lastResult = "failure" + } + } + } else triggered = false + const raced = await Promise.race(race) + if (raced) { + if (raced.done) break + triggered = true + } } - } - }) - return {} as HealthReceipt + }) + } + static of(effects: Effects, options: HealthCheckParams): HealthCheck { + return new HealthCheck(effects, options) + } + start() { + if (this.started) return + this.setStarted(performance.now()) + } + stop() { + if (!this.started) return + this.setStarted(null) + } + onDrop(): void { + this.exit() + } } + function asMessage(e: unknown) { if (object({ message: unknown }).test(e)) return String(e.message) const value = String(e) diff --git a/sdk/package/lib/health/index.ts b/sdk/package/lib/health/index.ts index b969037a5..1b9c46595 100644 --- a/sdk/package/lib/health/index.ts +++ b/sdk/package/lib/health/index.ts @@ -1 +1,3 @@ import "./checkFns" + +export { HealthCheck } from "./HealthCheck" diff --git a/sdk/package/lib/index.ts b/sdk/package/lib/index.ts index 3619765e1..dd301c6ee 100644 --- a/sdk/package/lib/index.ts +++ b/sdk/package/lib/index.ts @@ -7,7 +7,6 @@ import { ISB, IST, types, - T, matches, utils, } from "../../base/lib" @@ -21,10 +20,10 @@ export { ISB, IST, types, - T, matches, utils, } +export * as T from "./types" export { Daemons } from "./mainFn/Daemons" export { SubContainer } from "./util/SubContainer" export { StartSdk } from "./StartSdk" diff --git a/sdk/package/lib/mainFn/CommandController.ts b/sdk/package/lib/mainFn/CommandController.ts index d4ea171f0..23f798404 100644 --- a/sdk/package/lib/mainFn/CommandController.ts +++ b/sdk/package/lib/mainFn/CommandController.ts @@ -7,18 +7,20 @@ import { SubContainerHandle, SubContainer, } from "../util/SubContainer" -import { splitCommand } from "../util" +import { Drop, splitCommand } from "../util" import * as cp from "child_process" import * as fs from "node:fs/promises" -export class CommandController { +export class CommandController extends Drop { private constructor( readonly runningAnswer: Promise, private state: { exited: boolean }, private readonly subcontainer: SubContainer, private process: cp.ChildProcess, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, - ) {} + ) { + super() + } static of() { return async ( effects: T.Effects, @@ -33,7 +35,7 @@ export class CommandController { subcontainerName?: string // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number - mounts?: { path: string; options: MountOptions }[] + mounts?: { mountpoint: string; options: MountOptions }[] runAsInit?: boolean env?: | { @@ -68,7 +70,7 @@ export class CommandController { ) try { for (let mount of options.mounts || []) { - await subc.mount(mount.options, mount.path) + await subc.mount(mount.options, mount.mountpoint) } return subc } catch (e) { @@ -135,37 +137,42 @@ export class CommandController { return new SubContainerHandle(this.subcontainer) } async wait({ timeout = NO_TIMEOUT } = {}) { + const self = this.weak() if (timeout > 0) setTimeout(() => { - this.term() + self.term() }, timeout) try { - return await this.runningAnswer + return await self.runningAnswer } finally { - if (!this.state.exited) { - this.process.kill("SIGKILL") + if (!self.state.exited) { + self.process.kill("SIGKILL") } - await this.subcontainer.destroy().catch((_) => {}) + await self.subcontainer.destroy().catch((_) => {}) } } async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { + const self = this.weak() try { - if (!this.state.exited) { + if (!self.state.exited) { if (signal !== "SIGKILL") { setTimeout(() => { - if (!this.state.exited) this.process.kill("SIGKILL") + if (!self.state.exited) self.process.kill("SIGKILL") }, timeout) } - if (!this.process.kill(signal)) { + if (!self.process.kill(signal)) { console.error( `failed to send signal ${signal} to pid ${this.process.pid}`, ) } } - await this.runningAnswer + await self.runningAnswer } finally { - await this.subcontainer.destroy() + await self.subcontainer.destroy() } } + onDrop(): void { + this.term().catch(console.error) + } } diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index 864ae4122..8f254286c 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -29,7 +29,7 @@ export class Daemon { command: T.CommandType, options: { subcontainerName?: string - mounts?: { path: string; options: MountOptions }[] + mounts?: { mountpoint: string; options: MountOptions }[] env?: | { [variable: string]: string diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index e75ebec93..d471394c7 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -1,4 +1,4 @@ -import { HealthReceipt, Signals } from "../../../base/lib/types" +import { Signals } from "../../../base/lib/types" import { HealthCheckResult } from "../health/checkFns" @@ -15,6 +15,7 @@ export { CommandController } from "./CommandController" import { HealthDaemon } from "./HealthDaemon" import { Daemon } from "./Daemon" import { CommandController } from "./CommandController" +import { HealthCheck } from "../health/HealthCheck" export const cpExec = promisify(CP.exec) export const cpExecFile = promisify(CP.execFile) @@ -115,6 +116,7 @@ export class Daemons readonly daemons: Promise[], readonly ids: Ids[], readonly healthDaemons: HealthDaemon[], + readonly healthChecks: HealthCheck[], ) {} /** * Returns an empty new Daemons class with the provided inputSpec. @@ -129,7 +131,7 @@ export class Daemons static of(options: { effects: T.Effects started: (onTerm: () => PromiseLike) => PromiseLike - healthReceipts: HealthReceipt[] + healthChecks: HealthCheck[] }) { return new Daemons( options.effects, @@ -137,6 +139,7 @@ export class Daemons [], [], [], + options.healthChecks, ) } /** @@ -187,28 +190,33 @@ export class Daemons daemons, ids, healthDaemons, + this.healthChecks, ) } - async build() { - const built = { - term: async () => { - try { - for (let result of await Promise.allSettled( - this.healthDaemons.map((x) => - x.term({ timeout: x.sigtermTimeout }), - ), - )) { - if (result.status === "rejected") { - console.error(result.reason) - } - } - } finally { - this.effects.setMainStatus({ status: "stopped" }) + async term() { + try { + this.healthChecks.forEach((health) => health.stop()) + for (let result of await Promise.allSettled( + this.healthDaemons.map((x) => x.term({ timeout: x.sigtermTimeout })), + )) { + if (result.status === "rejected") { + console.error(result.reason) } - }, + } + } finally { + this.effects.setMainStatus({ status: "stopped" }) } - this.started(() => built.term()) - return built + } + + async build() { + for (const daemon of this.healthDaemons) { + await daemon.updateStatus() + } + for (const health of this.healthChecks) { + health.start() + } + this.started(() => this.term()) + return this } } diff --git a/sdk/package/lib/mainFn/HealthDaemon.ts b/sdk/package/lib/mainFn/HealthDaemon.ts index 3a1c1000f..5f9ea7f27 100644 --- a/sdk/package/lib/mainFn/HealthDaemon.ts +++ b/sdk/package/lib/mainFn/HealthDaemon.ts @@ -39,7 +39,6 @@ export class HealthDaemon { readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) { this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve)) - this.updateStatus() this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) } @@ -166,8 +165,8 @@ export class HealthDaemon { } as SetHealth) } - private async updateStatus() { - const healths = this.dependencies.map((d) => d._health) - this.changeRunning(healths.every((x) => x.result === "success")) + async updateStatus() { + const healths = this.dependencies.map((d) => d.running && d._health) + this.changeRunning(healths.every((x) => x && x.result === "success")) } } diff --git a/sdk/package/lib/mainFn/Mounts.ts b/sdk/package/lib/mainFn/Mounts.ts index 799140871..74d116957 100644 --- a/sdk/package/lib/mainFn/Mounts.ts +++ b/sdk/package/lib/mainFn/Mounts.ts @@ -1,7 +1,7 @@ import * as T from "../../../base/lib/types" import { MountOptions } from "../util/SubContainer" -type MountArray = { path: string; options: MountOptions }[] +type MountArray = { mountpoint: string; options: MountOptions }[] export class Mounts { private constructor( @@ -12,7 +12,6 @@ export class Mounts { readonly: boolean }[], readonly assets: { - id: Manifest["assets"][number] subpath: string | null mountpoint: string }[], @@ -49,15 +48,12 @@ export class Mounts { } addAssets( - /** The ID of the asset directory to mount. This is typically the same as the folder name in your assets directory */ - id: Manifest["assets"][number], /** The path within the asset directory to mount. Use `null` to mount the entire volume */ subpath: string | null, /** Where to mount the asset. e.g. /asset */ mountpoint: string, ) { this.assets.push({ - id, subpath, mountpoint, }) @@ -102,7 +98,7 @@ export class Mounts { return ([] as MountArray) .concat( this.volumes.map((v) => ({ - path: v.mountpoint, + mountpoint: v.mountpoint, options: { type: "volume", id: v.id, @@ -113,17 +109,16 @@ export class Mounts { ) .concat( this.assets.map((a) => ({ - path: a.mountpoint, + mountpoint: a.mountpoint, options: { type: "assets", - id: a.id, subpath: a.subpath, }, })), ) .concat( this.dependencies.map((d) => ({ - path: d.mountpoint, + mountpoint: d.mountpoint, options: { type: "pointer", packageId: d.dependencyId, diff --git a/sdk/package/lib/manifest/setupManifest.ts b/sdk/package/lib/manifest/setupManifest.ts index b0cb1edec..0995b3f51 100644 --- a/sdk/package/lib/manifest/setupManifest.ts +++ b/sdk/package/lib/manifest/setupManifest.ts @@ -16,10 +16,8 @@ import { execSync } from "child_process" export function setupManifest< Id extends string, VolumesTypes extends VolumeId, - AssetTypes extends VolumeId, Manifest extends { id: Id - assets: AssetTypes[] volumes: VolumesTypes[] } & SDKManifest, >(manifest: Manifest & SDKManifest): Manifest { @@ -31,12 +29,10 @@ export function buildManifest< Version extends string, Dependencies extends Record, VolumesTypes extends VolumeId, - AssetTypes extends VolumeId, ImagesTypes extends ImageId, Manifest extends { dependencies: Dependencies id: Id - assets: AssetTypes[] images: Record volumes: VolumesTypes[] }, diff --git a/sdk/package/lib/store/getStore.ts b/sdk/package/lib/store/getStore.ts index 131b7f7f7..cd086c979 100644 --- a/sdk/package/lib/store/getStore.ts +++ b/sdk/package/lib/store/getStore.ts @@ -18,7 +18,9 @@ export class GetStore { return this.effects.store.get({ ...this.options, path: extractJsonPath(this.path), - callback: () => this.effects.constRetry(), + callback: + this.effects.constRetry && + (() => this.effects.constRetry && this.effects.constRetry()), }) } /** diff --git a/sdk/package/lib/types.ts b/sdk/package/lib/types.ts new file mode 100644 index 000000000..0453a0681 --- /dev/null +++ b/sdk/package/lib/types.ts @@ -0,0 +1,2 @@ +export * from "../../base/lib/types" +export { HealthCheck } from "./health" diff --git a/sdk/package/lib/util/Drop.ts b/sdk/package/lib/util/Drop.ts new file mode 100644 index 000000000..e08883580 --- /dev/null +++ b/sdk/package/lib/util/Drop.ts @@ -0,0 +1,26 @@ +export abstract class Drop { + private static weak: { [id: number]: Drop } = {} + private static registry = new FinalizationRegistry((id: number) => { + Drop.weak[id].drop() + }) + private static idCtr: number = 0 + private id: number + private ref: { id: number } | WeakRef<{ id: number }> + protected constructor() { + this.id = Drop.idCtr++ + this.ref = { id: this.id } + Drop.weak[this.id] = this.weak() + Drop.registry.register(this, this.id, this) + } + protected weak(): this { + const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), this) + weak.ref = new WeakRef(this.ref) + return weak + } + abstract onDrop(): void + drop(): void { + this.onDrop() + Drop.registry.unregister(this) + delete Drop.weak[this.id] + } +} diff --git a/sdk/package/lib/util/GetSslCertificate.ts b/sdk/package/lib/util/GetSslCertificate.ts index 8ea870ffb..afc12e6b2 100644 --- a/sdk/package/lib/util/GetSslCertificate.ts +++ b/sdk/package/lib/util/GetSslCertificate.ts @@ -15,7 +15,9 @@ export class GetSslCertificate { return this.effects.getSslCertificate({ hostnames: this.hostnames, algorithm: this.algorithm, - callback: () => this.effects.constRetry(), + callback: + this.effects.constRetry && + (() => this.effects.constRetry && this.effects.constRetry()), }) } /** diff --git a/sdk/package/lib/util/SubContainer.ts b/sdk/package/lib/util/SubContainer.ts index 21dcc75c7..912c3112a 100644 --- a/sdk/package/lib/util/SubContainer.ts +++ b/sdk/package/lib/util/SubContainer.ts @@ -130,14 +130,14 @@ export class SubContainer implements ExecSpawnable { static async with( effects: T.Effects, image: { imageId: T.ImageId; sharedRun?: boolean }, - mounts: { options: MountOptions; path: string }[], + mounts: { options: MountOptions; mountpoint: string }[], name: string, fn: (subContainer: SubContainer) => Promise, ): Promise { const subContainer = await SubContainer.of(effects, image, name) try { for (let mount of mounts) { - await subContainer.mount(mount.options, mount.path) + await subContainer.mount(mount.options, mount.mountpoint) } return await fn(subContainer) } finally { @@ -166,7 +166,7 @@ export class SubContainer implements ExecSpawnable { ? options.subpath : `/${options.subpath}` : "/" - const from = `/media/startos/assets/${options.id}${subpath}` + const from = `/media/startos/assets/${subpath}` await fs.mkdir(from, { recursive: true }) await fs.mkdir(path, { recursive: true }) @@ -449,7 +449,6 @@ export type MountOptionsVolume = { export type MountOptionsAssets = { type: "assets" - id: string subpath: string | null } diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index 74b7b7083..83f46d94d 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -3,7 +3,7 @@ import * as YAML from "yaml" import * as TOML from "@iarna/toml" import * as T from "../../../base/lib/types" import * as fs from "node:fs/promises" -import { asError } from "../../../base/lib/util" +import { asError, partialDiff } from "../../../base/lib/util" const previousPath = /(.+?)\/([^/]*)$/ @@ -101,6 +101,7 @@ function fileMerge(...args: any[]): any { * ``` */ export class FileHelper { + private consts: (() => void)[] = [] protected constructor( readonly path: string, readonly writeData: (dataIn: A) => string, @@ -108,27 +109,37 @@ export class FileHelper { readonly validate: (value: unknown) => A, ) {} - /** - * Accepts structured data and overwrites the existing file on disk. - */ - private async writeFile(data: A): Promise { + private async writeFileRaw(data: string): Promise { const parent = previousPath.exec(this.path) if (parent) { await fs.mkdir(parent[1], { recursive: true }) } - await fs.writeFile(this.path, this.writeData(data)) + await fs.writeFile(this.path, data) return null } - private async readFile(): Promise { + /** + * Accepts structured data and overwrites the existing file on disk. + */ + private async writeFile(data: A): Promise { + return await this.writeFileRaw(this.writeData(data)) + } + + private async readFileRaw(): Promise { if (!(await exists(this.path))) { return null } - return this.readData( - await fs.readFile(this.path).then((data) => data.toString("utf-8")), - ) + return await fs.readFile(this.path).then((data) => data.toString("utf-8")) + } + + private async readFile(): Promise { + const raw = await this.readFileRaw() + if (raw === null) { + return raw + } + return this.readData(raw) } /** @@ -143,7 +154,14 @@ export class FileHelper { private async readConst(effects: T.Effects): Promise { const watch = this.readWatch() const res = await watch.next() - watch.next().then(effects.constRetry) + if (effects.constRetry) { + if (!this.consts.includes(effects.constRetry)) + this.consts.push(effects.constRetry) + watch.next().then(() => { + this.consts = this.consts.filter((a) => a === effects.constRetry) + effects.constRetry && effects.constRetry() + }) + } return res.value } @@ -213,17 +231,35 @@ export class FileHelper { /** * Accepts full structured data and overwrites the existing file on disk if it exists. */ - async write(data: A) { - return await this.writeFile(this.validate(data)) + async write(effects: T.Effects, data: A) { + await this.writeFile(this.validate(data)) + if (effects.constRetry && this.consts.includes(effects.constRetry)) + throw new Error(`Canceled: write after const: ${this.path}`) + return null } /** * Accepts partial structured data and performs a merge with the existing file on disk. */ - async merge(data: T.DeepPartial) { - const fileData = (await this.readFile()) || null - const mergeData = fileMerge(fileData, data) - return await this.writeFile(this.validate(mergeData)) + async merge(effects: T.Effects, data: T.DeepPartial) { + const fileDataRaw = await this.readFileRaw() + let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw) + try { + fileData = this.validate(fileData) + } catch (_) {} + const mergeData = this.validate(fileMerge({}, fileData, data)) + const toWrite = this.writeData(mergeData) + if (toWrite !== fileDataRaw) { + this.writeFile(mergeData) + if (effects.constRetry && this.consts.includes(effects.constRetry)) { + const diff = partialDiff(fileData, mergeData as any) + if (!diff) { + return null + } + throw new Error(`Canceled: write after const: ${this.path}`) + } + } + return null } /** diff --git a/sdk/package/lib/util/index.ts b/sdk/package/lib/util/index.ts index 66c73503e..d5f024686 100644 --- a/sdk/package/lib/util/index.ts +++ b/sdk/package/lib/util/index.ts @@ -2,3 +2,4 @@ export * from "../../../base/lib/util" export { GetSslCertificate } from "./GetSslCertificate" export { hostnameInfoToAddress } from "../../../base/lib/util/Hostname" +export { Drop } from "./Drop" diff --git a/sdk/package/lib/version/VersionGraph.ts b/sdk/package/lib/version/VersionGraph.ts index 91c7a0cc0..129153e52 100644 --- a/sdk/package/lib/version/VersionGraph.ts +++ b/sdk/package/lib/version/VersionGraph.ts @@ -163,15 +163,17 @@ export class VersionGraph { (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), - ).reduce( - (acc, x) => - acc.or( - x.metadata instanceof VersionRange - ? x.metadata - : VersionRange.anchor("=", x.metadata), - ), - VersionRange.none(), - ), + ) + .reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ) + .normalize(), ) canMigrateTo = once(() => Array.from( @@ -182,15 +184,17 @@ export class VersionGraph { (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), - ).reduce( - (acc, x) => - acc.or( - x.metadata instanceof VersionRange - ? x.metadata - : VersionRange.anchor("=", x.metadata), - ), - VersionRange.none(), - ), + ) + .reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ) + .normalize(), ) } diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index d5adb8c6a..a98b89158 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,17 +1,18 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.14", + "version": "0.3.6-beta.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.14", + "version": "0.3.6-beta.18", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", + "deep-equality-data-structures": "^1.5.1", "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", @@ -2287,6 +2288,15 @@ } } }, + "node_modules/deep-equality-data-structures": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-1.5.1.tgz", + "integrity": "sha512-P7zsL2/AbZIGHDxbo/LLEhCp11AttRp8GvzXOXudqMT/qiGCLo/pyI4lAZvjUZyQnlIbPna3fv8DMsuRvLt4ww==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4016,6 +4026,15 @@ "node": ">=8" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/sdk/package/package.json b/sdk/package/package.json index e0f4ed04e..8cdcd94ad 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.14", + "version": "0.3.6-beta.18", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", @@ -36,6 +36,7 @@ "mime-types": "^2.1.35", "ts-matches": "^6.2.1", "yaml": "^2.2.2", + "deep-equality-data-structures": "^1.5.1", "@iarna/toml": "^2.2.5", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0" diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index adea02c66..ca4a54073 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -117,7 +117,6 @@ export namespace Mock { emulateMissingAs: 'aarch64', }, }, - assets: [], volumes: ['main'], hardwareRequirements: { device: [], @@ -174,7 +173,6 @@ export namespace Mock { emulateMissingAs: 'aarch64', }, }, - assets: [], volumes: ['main'], hardwareRequirements: { device: [], @@ -224,7 +222,6 @@ export namespace Mock { emulateMissingAs: 'aarch64', }, }, - assets: [], volumes: ['main'], hardwareRequirements: { device: [],