Skip to content

Commit cd43725

Browse files
committed
update manifest
1 parent 6c7c7cb commit cd43725

4 files changed

Lines changed: 175 additions & 45 deletions

File tree

sbuild-meta/src/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ enum Commands {
6060
/// GitHub token for registry access
6161
#[arg(long, env = "GITHUB_TOKEN")]
6262
github_token: Option<String>,
63+
64+
/// GHCR owner/organization (default: pkgforge)
65+
#[arg(long, default_value = "pkgforge")]
66+
ghcr_owner: String,
6367
},
6468

6569
/// Check if a recipe should be rebuilt
@@ -164,8 +168,9 @@ async fn main() -> Result<()> {
164168
cache,
165169
parallel,
166170
github_token,
171+
ghcr_owner,
167172
} => {
168-
cmd_generate(arch, recipes, output, cache_type, cache, parallel, github_token).await
173+
cmd_generate(arch, recipes, output, cache_type, cache, parallel, github_token, ghcr_owner).await
169174
}
170175

171176
Commands::ShouldRebuild {
@@ -212,6 +217,7 @@ async fn cmd_generate(
212217
_cache: Option<PathBuf>,
213218
_parallel: usize,
214219
github_token: Option<String>,
220+
ghcr_owner: String,
215221
) -> Result<()> {
216222
info!("Generating metadata for {} (cache: {})", arch, cache_type_filter);
217223

@@ -239,7 +245,7 @@ async fn cmd_generate(
239245

240246
for (path, recipe) in recipes {
241247
// Get all GHCR packages for this recipe (handles multiple binaries)
242-
let ghcr_packages = recipe.ghcr_packages_from_path(&path);
248+
let ghcr_packages = recipe.ghcr_packages_from_path(&path, &ghcr_owner);
243249

244250
if ghcr_packages.is_empty() {
245251
warn!("No GHCR packages found for {:?}", path);
@@ -284,7 +290,7 @@ async fn cmd_generate(
284290
match client.fetch_manifest(&ghcr_info.ghcr_path, tag).await {
285291
Ok(manifest_str) => {
286292
if let Ok(manifest) = OciManifest::from_json(&manifest_str) {
287-
metadata.enrich_from_manifest(&manifest, tag);
293+
metadata.enrich_from_manifest(&manifest, &ghcr_info.ghcr_path, tag);
288294
}
289295
}
290296
Err(e) => {

sbuild-meta/src/metadata.rs

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ pub struct PackageMetadata {
183183

184184
#[serde(skip_serializing_if = "is_empty_vec")]
185185
pub replaces: Option<Vec<String>>,
186+
187+
#[serde(skip_serializing_if = "is_empty_vec")]
188+
pub repology: Option<Vec<String>>,
186189
}
187190

188191
impl PackageMetadata {
@@ -236,63 +239,87 @@ impl PackageMetadata {
236239
} else {
237240
Some(recipe.provides.clone())
238241
},
242+
repology: if recipe.repology.is_empty() {
243+
None
244+
} else {
245+
Some(recipe.repology.clone())
246+
},
239247
disabled: if recipe.disabled { Some(true) } else { None },
240248
..Default::default()
241249
}
242250
}
243251

244252
/// Enrich metadata with OCI manifest data
245-
pub fn enrich_from_manifest(&mut self, manifest: &OciManifest, tag: &str) {
253+
///
254+
/// `ghcr_path` is the repository path (e.g., "pkgforge/bincache/hello/static")
255+
pub fn enrich_from_manifest(&mut self, manifest: &OciManifest, ghcr_path: &str, tag: &str) {
246256
// Get embedded JSON if available
247257
if let Ok(Some(pkg_json)) = manifest.get_package_json() {
248258
self.merge_from_json(&pkg_json);
249259
}
250260

251-
// GHCR info
252-
self.ghcr_pkg = manifest.ghcr_pkg().map(|s| s.to_string());
261+
// GHCR info - construct from path and tag
262+
let ghcr_pkg = format!("ghcr.io/{}:{}", ghcr_path, tag);
263+
self.ghcr_pkg = Some(ghcr_pkg.clone());
264+
self.ghcr_url = Some(format!("https://ghcr.io/{}", ghcr_path));
265+
253266
let size = manifest.total_size();
254267
self.ghcr_size_raw = Some(size);
255268
self.ghcr_size = Some(format_size(size));
256269
self.ghcr_files = Some(manifest.filenames().into_iter().map(|s| s.to_string()).collect());
257270

271+
// Version from annotations
272+
if let Some(version) = manifest.get_annotation("dev.pkgforge.soar.version") {
273+
self.version = version.to_string();
274+
} else if let Some(version) = manifest.get_annotation("org.opencontainers.image.version") {
275+
self.version = version.to_string();
276+
}
277+
278+
// Build date from annotations
279+
if let Some(date) = manifest.get_annotation("dev.pkgforge.soar.push_date") {
280+
self.build_date = Some(date.to_string());
281+
} else if let Some(date) = manifest.get_annotation("org.opencontainers.image.created") {
282+
self.build_date = Some(date.to_string());
283+
}
284+
258285
// Build info from annotations
259286
if self.build_id.is_none() {
260287
let build_id = manifest.build_id().map(|s| s.to_string());
261288
self.build_id = build_id.clone();
262289

263290
// Generate GitHub Actions URL if we have a build ID
264291
if let Some(ref id) = build_id {
265-
// Try to determine the repo from ghcr_pkg
266-
if let Some(ref ghcr_pkg) = self.ghcr_pkg {
267-
let cache_type = if ghcr_pkg.contains("pkgcache") { "pkgcache" } else { "bincache" };
268-
self.build_gha = Some(format!(
269-
"https://github.com/pkgforge/{}/actions/runs/{}",
270-
cache_type, id
271-
));
272-
}
292+
let cache_type = if ghcr_path.contains("pkgcache") { "pkgcache" } else { "bincache" };
293+
self.build_gha = Some(format!(
294+
"https://github.com/pkgforge/{}/actions/runs/{}",
295+
cache_type, id
296+
));
297+
}
298+
}
299+
300+
// Build script from annotations
301+
if self.build_script.is_none() {
302+
if let Some(script) = manifest.get_annotation("dev.pkgforge.soar.build_script") {
303+
self.build_script = Some(script.to_string());
273304
}
274305
}
275306

276-
// Generate blob reference for main binary
307+
// Generate blob reference and download URLs
277308
if let Some(filename) = manifest.filenames().first() {
278309
self.ghcr_blob = manifest.get_blob_ref(filename);
279310

280311
// Generate download URL and manifest URL
281-
if let Some(ref ghcr_pkg) = self.ghcr_pkg {
282-
let base = ghcr_pkg.split(':').next().unwrap_or(ghcr_pkg);
283-
let repo = base.replace("ghcr.io/", "");
284-
self.download_url = format!(
285-
"https://api.ghcr.pkgforge.dev/{}?tag={}&download={}",
286-
repo, tag, filename
287-
);
288-
self.manifest_url = Some(format!(
289-
"https://api.ghcr.pkgforge.dev/{}?tag={}&manifest",
290-
repo, tag
291-
));
292-
// Size is usually same as ghcr_size for single binary packages
293-
self.size_raw = self.ghcr_size_raw;
294-
self.size = self.ghcr_size.clone();
295-
}
312+
self.download_url = format!(
313+
"https://api.ghcr.pkgforge.dev/{}?tag={}&download={}",
314+
ghcr_path, tag, filename
315+
);
316+
self.manifest_url = Some(format!(
317+
"https://api.ghcr.pkgforge.dev/{}?tag={}&manifest",
318+
ghcr_path, tag
319+
));
320+
// Size is usually same as ghcr_size for single binary packages
321+
self.size_raw = self.ghcr_size_raw;
322+
self.size = self.ghcr_size.clone();
296323
}
297324
}
298325

@@ -403,8 +430,8 @@ impl MetadataBuilder {
403430
}
404431
}
405432

406-
pub fn with_manifest(mut self, manifest: &OciManifest, tag: &str) -> Self {
407-
self.metadata.enrich_from_manifest(manifest, tag);
433+
pub fn with_manifest(mut self, manifest: &OciManifest, ghcr_path: &str, tag: &str) -> Self {
434+
self.metadata.enrich_from_manifest(manifest, ghcr_path, tag);
408435
self
409436
}
410437

sbuild-meta/src/recipe.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ impl SBuildRecipe {
350350
}
351351

352352
/// GHCR package information including path components
353-
pub fn ghcr_packages_from_path(&self, recipe_path: &Path) -> Vec<GhcrPackageInfo> {
353+
pub fn ghcr_packages_from_path(&self, recipe_path: &Path, ghcr_owner: &str) -> Vec<GhcrPackageInfo> {
354354
let mut packages = Vec::new();
355355

356356
// Determine cache type based on path (bincache for binaries/, pkgcache for packages/)
@@ -382,8 +382,8 @@ impl SBuildRecipe {
382382
// GHCR path: {owner}/{cache}/{pkg_family}/{recipe_name}
383383
// e.g., pkgforge/bincache/hello/static
384384
let ghcr_path = format!(
385-
"pkgforge/{}/{}/{}",
386-
cache_type, pkg_family, recipe_name
385+
"{}/{}/{}/{}",
386+
ghcr_owner, cache_type, pkg_family, recipe_name
387387
);
388388

389389
packages.push(GhcrPackageInfo {
@@ -516,7 +516,7 @@ provides:
516516
"#;
517517
let recipe = SBuildRecipe::from_yaml(yaml).unwrap();
518518
let path = Path::new("binaries/bat/static.yaml");
519-
let packages = recipe.ghcr_packages_from_path(path);
519+
let packages = recipe.ghcr_packages_from_path(path, "pkgforge");
520520

521521
// bat==batcat means bat is the package, batcat is symlink - only 1 entry
522522
assert_eq!(packages.len(), 1);
@@ -536,7 +536,7 @@ provides:
536536
"#;
537537
let recipe = SBuildRecipe::from_yaml(yaml).unwrap();
538538
let path = Path::new("binaries/myapp/static.yaml");
539-
let packages = recipe.ghcr_packages_from_path(path);
539+
let packages = recipe.ghcr_packages_from_path(path, "pkgforge");
540540

541541
// Two separate packages - but same GHCR path (they're in the same recipe)
542542
assert_eq!(packages.len(), 2);
@@ -556,7 +556,7 @@ provides:
556556
"#;
557557
let recipe = SBuildRecipe::from_yaml(yaml).unwrap();
558558
let path = Path::new("binaries/busybox/static.yaml");
559-
let packages = recipe.ghcr_packages_from_path(path);
559+
let packages = recipe.ghcr_packages_from_path(path, "pkgforge");
560560

561561
// All entries refer to busybox - should deduplicate to 1
562562
assert_eq!(packages.len(), 1);
@@ -574,7 +574,7 @@ provides:
574574
"#;
575575
let recipe = SBuildRecipe::from_yaml(yaml).unwrap();
576576
let path = Path::new("packages/0ad/appimage.0ad-matters.stable.yaml");
577-
let packages = recipe.ghcr_packages_from_path(path);
577+
let packages = recipe.ghcr_packages_from_path(path, "pkgforge");
578578

579579
assert_eq!(packages.len(), 1);
580580
// New simplified format: {owner}/{cache}/{pkg_family}/{recipe_name}

sbuild/src/main.rs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use std::{
66
env,
7+
fs,
78
path::{Path, PathBuf},
89
process::Command,
910
sync::{
@@ -68,6 +69,95 @@ fn parse_ghcr_path(recipe_path: &str) -> Option<(String, String)> {
6869
Some((pkg_family, recipe_name))
6970
}
7071

72+
/// Metadata extracted from SBUILD file for GHCR annotations
73+
#[derive(Debug, Default)]
74+
struct SbuildMetadata {
75+
pkg: String,
76+
pkg_id: String,
77+
pkg_type: Option<String>,
78+
description: Option<String>,
79+
homepage: Option<String>,
80+
license: Option<String>,
81+
}
82+
83+
/// Read metadata from SBUILD file in output directory
84+
fn read_sbuild_metadata(outdir: &Path) -> Option<SbuildMetadata> {
85+
let sbuild_path = outdir.join("SBUILD");
86+
let content = fs::read_to_string(&sbuild_path).ok()?;
87+
88+
// Parse YAML content
89+
let yaml: serde_yml::Value = serde_yml::from_str(&content).ok()?;
90+
let map = yaml.as_mapping()?;
91+
92+
let pkg = map
93+
.get("pkg")
94+
.and_then(|v| v.as_str())
95+
.unwrap_or("unknown")
96+
.to_string();
97+
98+
let pkg_id = map
99+
.get("pkg_id")
100+
.and_then(|v| v.as_str())
101+
.unwrap_or("unknown")
102+
.to_string();
103+
104+
let pkg_type = map.get("pkg_type").and_then(|v| v.as_str()).map(String::from);
105+
106+
// Description can be a string or a map with short/long
107+
let description = map.get("description").and_then(|v| {
108+
if let Some(s) = v.as_str() {
109+
Some(s.to_string())
110+
} else if let Some(m) = v.as_mapping() {
111+
m.get("short").and_then(|s| s.as_str()).map(String::from)
112+
} else {
113+
None
114+
}
115+
});
116+
117+
// Homepage is an array, take the first one
118+
let homepage = map.get("homepage").and_then(|v| {
119+
if let Some(arr) = v.as_sequence() {
120+
arr.first().and_then(|s| s.as_str()).map(String::from)
121+
} else {
122+
v.as_str().map(String::from)
123+
}
124+
});
125+
126+
// License is an array of strings or complex objects
127+
let license = map.get("license").and_then(|v| {
128+
if let Some(arr) = v.as_sequence() {
129+
let licenses: Vec<String> = arr
130+
.iter()
131+
.filter_map(|item| {
132+
if let Some(s) = item.as_str() {
133+
Some(s.to_string())
134+
} else if let Some(m) = item.as_mapping() {
135+
m.get("id").and_then(|id| id.as_str()).map(String::from)
136+
} else {
137+
None
138+
}
139+
})
140+
.collect();
141+
if licenses.is_empty() {
142+
None
143+
} else {
144+
Some(licenses.join(", "))
145+
}
146+
} else {
147+
v.as_str().map(String::from)
148+
}
149+
});
150+
151+
Some(SbuildMetadata {
152+
pkg,
153+
pkg_id,
154+
pkg_type,
155+
description,
156+
homepage,
157+
license,
158+
})
159+
}
160+
71161
/// Log level for build output
72162
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
73163
enum LogLevel {
@@ -384,14 +474,21 @@ async fn post_build_processing(
384474
(base_repo.clone(), pkg_name.unwrap_or("unknown").to_string())
385475
};
386476

477+
// Read metadata from SBUILD file
478+
let metadata = read_sbuild_metadata(outdir).unwrap_or_default();
479+
387480
let annotations = PackageAnnotations {
388-
pkg: pkg.clone(),
389-
pkg_id: "unknown".to_string(),
390-
pkg_type: None,
481+
pkg: if metadata.pkg.is_empty() || metadata.pkg == "unknown" {
482+
pkg.clone()
483+
} else {
484+
metadata.pkg
485+
},
486+
pkg_id: metadata.pkg_id,
487+
pkg_type: metadata.pkg_type,
391488
version: version.clone(),
392-
description: None,
393-
homepage: None,
394-
license: None,
489+
description: metadata.description,
490+
homepage: metadata.homepage,
491+
license: metadata.license,
395492
build_date: chrono::Utc::now().to_rfc3339(),
396493
build_id: env::var("GITHUB_RUN_ID").ok(),
397494
build_gha: env::var("GITHUB_RUN_ID")

0 commit comments

Comments
 (0)