try parse manifest

This commit is contained in:
2025-02-12 16:51:55 +00:00
parent 63cff68bdd
commit f90187c914
5 changed files with 304 additions and 232 deletions

350
Cargo.lock generated
View File

@ -27,17 +27,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.11"
@ -59,6 +48,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.18"
@ -123,7 +127,7 @@ checksum = "1cb37b529daddddf129612580831db21a538d1aa2798b367039e9316762c81a7"
dependencies = [
"anyhow",
"byteorder",
"quick-xml",
"quick-xml 0.26.0",
"rasn",
"rasn-pkix",
"roxmltree",
@ -132,16 +136,7 @@ dependencies = [
"sha2",
"tracing",
"xcommon",
"zip 0.6.6",
]
[[package]]
name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
"zip",
]
[[package]]
@ -156,6 +151,24 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-compression"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [
"bzip2",
"deflate64",
"flate2",
"futures-core",
"futures-io",
"memchr",
"pin-project-lite",
"xz2",
"zstd",
"zstd-safe",
]
[[package]]
name = "async-trait"
version = "0.1.86"
@ -198,6 +211,22 @@ dependencies = [
"web-sys",
]
[[package]]
name = "async_zip"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"chrono",
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 1.0.69",
"tokio",
"tokio-util",
]
[[package]]
name = "atomic-destructor"
version = "0.3.0"
@ -386,9 +415,9 @@ dependencies = [
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
version = "0.1.12+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9"
dependencies = [
"cc",
"libc",
@ -451,7 +480,10 @@ version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets",
]
[[package]]
@ -575,12 +607,6 @@ dependencies = [
"tiny-keccak",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.6.0"
@ -615,21 +641,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
@ -685,26 +696,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "dialoguer"
version = "0.11.0"
@ -919,6 +910,30 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@ -940,6 +955,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@ -1198,6 +1214,29 @@ dependencies = [
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -1500,12 +1539,6 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.25"
@ -1513,13 +1546,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "lzma-rs"
version = "0.3.0"
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"byteorder",
"crc",
"cc",
"libc",
"pkg-config",
]
[[package]]
@ -1568,6 +1602,7 @@ dependencies = [
"anyhow",
"apk",
"async-trait",
"async_zip",
"clap",
"config",
"dialoguer",
@ -1575,11 +1610,12 @@ dependencies = [
"indicatif",
"log",
"nostr-sdk",
"quick-xml 0.37.2",
"reqwest",
"semver",
"serde",
"sha2",
"tokio",
"zip 2.2.2",
]
[[package]]
@ -1712,12 +1748,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@ -1829,6 +1859,12 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "password-hash"
version = "0.5.0"
@ -1925,6 +1961,26 @@ dependencies = [
"sha2",
]
[[package]]
name = "pin-project"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@ -1995,12 +2051,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -2029,6 +2079,16 @@ dependencies = [
"serde",
]
[[package]]
name = "quick-xml"
version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quote"
version = "1.0.38"
@ -2176,11 +2236,13 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry",
]
@ -2710,25 +2772,6 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -2846,6 +2889,7 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@ -3173,6 +3217,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.77"
@ -3202,6 +3259,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
@ -3368,7 +3434,7 @@ dependencies = [
"rasn-pkix",
"rsa",
"sha2",
"zip 0.6.6",
"zip",
]
[[package]]
@ -3377,6 +3443,15 @@ version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-rust2"
version = "0.9.0"
@ -3459,20 +3534,6 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zerovec"
@ -3508,49 +3569,6 @@ dependencies = [
"flate2",
]
[[package]]
name = "zip"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"displaydoc",
"flate2",
"hmac",
"indexmap",
"lzma-rs",
"memchr",
"pbkdf2",
"rand",
"sha1",
"thiserror 2.0.11",
"time",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.2"

View File

@ -16,13 +16,15 @@ clap = { version = "4.5.28", features = ["derive"] }
config = { version = "0.15.7", features = ["yaml"] }
log = "0.4.25"
nostr-sdk = "0.39.0"
reqwest = { version = "0.12.12", features = ["json"] }
reqwest = { version = "0.12.12", features = ["json", "stream"] }
tokio = { version = "1.43.0", features = ["fs", "rt", "macros", "rt-multi-thread"] }
serde = { version = "1.0.217", features = ["derive"] }
async-trait = "0.1.86"
apk = "0.4.0"
zip = "2.2.2"
async_zip = { version = "0.0.17", features = ["full", "tokio"] }
semver = "1.0.25"
indicatif = "0.17.11"
dialoguer = "0.11.0"
env_logger = "0.11.6"
sha2 = "0.10.8"
quick-xml = { version = "0.37.2", features = ["serialize"] }

View File

@ -20,12 +20,11 @@ struct Args {
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
// Set default log level to info
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
env_logger::init();
let args = Args::parse();

View File

@ -1,4 +1,6 @@
use crate::repo::{Repo, RepoArtifact, RepoRelease, RepoResource};
use crate::repo::{
load_artifact, load_artifact_url, Repo, RepoArtifact, RepoRelease, RepoResource,
};
use anyhow::{anyhow, Result};
use log::{info, warn};
use nostr_sdk::Url;
@ -60,46 +62,6 @@ struct GithubReleaseArtifact {
pub browser_download_url: String,
}
impl TryFrom<&GithubRelease> for RepoRelease {
type Error = anyhow::Error;
fn try_from(value: &GithubRelease) -> std::result::Result<Self, Self::Error> {
Ok(RepoRelease {
version: Version::parse(if value.tag_name.starts_with("v") {
&value.tag_name[1..]
} else {
&value.tag_name
})?,
artifacts: value
.assets
.iter()
.filter_map(|v| match RepoArtifact::try_from(v) {
Ok(art) => Some(art),
Err(e) => {
warn!("Failed to parse artifact {}: {}", &v.name, e);
None
}
})
.collect(),
})
}
}
impl TryFrom<&GithubReleaseArtifact> for RepoArtifact {
type Error = anyhow::Error;
fn try_from(value: &GithubReleaseArtifact) -> std::result::Result<Self, Self::Error> {
Ok(RepoArtifact {
name: value.name.clone(),
size: value.size,
content_type: value.content_type.clone(),
platform: Platform::IOS,
location: RepoResource::Remote(value.browser_download_url.clone()),
metadata: (),
})
}
}
#[async_trait::async_trait]
impl Repo for GithubRepo {
async fn get_releases(&self) -> Result<Vec<RepoRelease>> {
@ -115,16 +77,33 @@ impl Repo for GithubRepo {
))
.build()?;
let rsp: Vec<GithubRelease> = self.client.execute(req).await?.json().await?;
Ok(rsp
.into_iter()
.filter_map(|v| match RepoRelease::try_from(&v) {
Ok(r) => Some(r),
Err(e) => {
warn!("Failed to parse release: {} {}", v.tag_name, e);
None
let gh_release: Vec<GithubRelease> = self.client.execute(req).await?.json().await?;
let mut releases = vec![];
for release in gh_release {
let mut artifacts = vec![];
for gh_artifact in release.assets {
match load_artifact_url(&gh_artifact.browser_download_url).await {
Ok(a) => artifacts.push(a),
Err(e) => warn!(
"Failed to load artifact {}: {}",
gh_artifact.browser_download_url, e
),
}
})
.collect())
}
if artifacts.is_empty() {
warn!("No artifacts found for {}", release.tag_name);
continue;
}
releases.push(RepoRelease {
version: Version::parse(if release.tag_name.starts_with("v") {
&release.tag_name[1..]
} else {
&release.tag_name
})?,
artifacts,
});
}
Ok(releases)
}
}

View File

@ -1,9 +1,20 @@
use std::path::{Path, PathBuf};
use crate::manifest::Manifest;
use crate::repo::github::GithubRepo;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail, Context, Result};
use apk::AndroidManifest;
use async_zip::tokio::read::seek::ZipFileReader;
use async_zip::ZipFile;
use log::info;
use nostr_sdk::async_utility::futures_util::TryStreamExt;
use nostr_sdk::prelude::{hex, StreamExt};
use reqwest::Url;
use semver::Version;
use serde::Deserialize;
use sha2::Digest;
use std::env::temp_dir;
use std::path::{Path, PathBuf};
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufReader};
mod github;
@ -14,16 +25,11 @@ pub struct RepoArtifact {
pub location: RepoResource,
pub content_type: String,
pub platform: Platform,
pub metadata: ArtifactMetadata
pub metadata: ArtifactMetadata,
}
pub enum ArtifactMetadata {
APK {
version_code: u32,
min_sdk_version: u32,
target_sdk_version: u32,
sig_hash: String,
}
APK { manifest: AndroidManifest },
}
pub enum Platform {
@ -79,10 +85,78 @@ impl TryInto<Box<dyn Repo>> for &Manifest {
}
}
/// Download an artifact and create a [RepoArtifact]
async fn load_artifact_url(url: &str) -> Result<RepoArtifact> {
info!("Downloading artifact {}", url);
let u = Url::parse(url)?;
let rsp = reqwest::get(u.clone()).await?;
let id = hex::encode(sha2::Sha256::digest(url.as_bytes()));
let mut tmp = temp_dir().join(id);
tmp.set_extension(
PathBuf::from(u.path())
.extension()
.ok_or(anyhow!("Missing extension in URL"))?
.to_str()
.unwrap(),
);
if !tmp.exists() {
let mut tmp_file = File::create(&tmp).await?;
let mut rsp_stream = rsp.bytes_stream();
while let Some(data) = rsp_stream.next().await {
if let Ok(data) = data {
tmp_file.write_all(&data).await?;
}
}
}
load_artifact(&tmp).await
}
async fn load_artifact(path: &Path) -> Result<RepoArtifact> {
match path
.extension()
.ok_or(anyhow!("missing file extension"))?
.to_str()
.unwrap()
{
"apk" => load_apk_artifact(path).await,
_ => bail!("unknown file extension"),
}
}
}
async fn load_apk_artifact(path: &Path) -> Result<RepoArtifact> {
let file = File::open(path).await?;
let mut zip = ZipFileReader::with_tokio(BufReader::new(file)).await?;
const ANDROID_MANIFEST: &'static str = "AndroidManifest.xml";
let idx = zip
.file()
.entries()
.iter()
.enumerate()
.find_map(|(i, entry)| {
if entry.filename().as_bytes() == ANDROID_MANIFEST.as_bytes() {
Some(i)
} else {
None
}
})
.ok_or(anyhow!("missing AndroidManifest file"))?;
let mut manifest = zip.reader_with_entry(idx).await?;
let mut manifest_data = String::with_capacity(8192);
manifest.read_to_string_checked(&mut manifest_data).await?;
info!("Successfully loaded AndroidManifest: {}", &manifest_data);
let manifest: AndroidManifest = quick_xml::de::from_str(&manifest_data)?;
Ok(RepoArtifact {
name: path.file_name().unwrap().to_str().unwrap().to_string(),
size: path.metadata()?.len(),
location: RepoResource::Local(path.to_path_buf()),
content_type: "application/apk".to_string(),
platform: Platform::Android {
arch: Architecture::ARMv8,
},
metadata: ArtifactMetadata::APK { manifest },
})
}