bakery: add license_file/desktop_file/data_archive manifest fields
All checks were successful
dev bakery / build (push) Successful in 38s
All checks were successful
dev bakery / build (push) Successful in 38s
Closes the packaging gap found while moving bread-ecosystem apps off pacman onto bakery-only distribution: pacman's package() typically installs a LICENSE file and, for GUI/onboarding apps, a .desktop entry and sometimes a data directory (e.g. breadhelp's guide content). All three follow the same download-verify-place pattern ConfigScaffold.example already established: - license_file -> ~/.local/share/licenses/<name>/LICENSE - desktop_file -> ~/.local/share/applications/<name>.desktop - data_archive -> a .tar.gz extracted to ~/.local/share/<name>/ (for arbitrary data too big/structured for a single file, via `tar`) gen-index.sh parses all three from bakery.toml, hashes the artifact, and now excludes them from the binaries-collection loop (previously undetected gap: they'd have been swept in as fake "binaries" with no checksum, same class of bug the existing .toml/.service/etc exclusions guard against). Also registers breadhelp as a bakery-channel product.
This commit is contained in:
parent
5afe12d70f
commit
c7abfae630
4 changed files with 459 additions and 16 deletions
|
|
@ -23,19 +23,34 @@ pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> {
|
||||||
scaffold_config(cfg, pkg)?;
|
scaffold_config(cfg, pkg)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Install systemd user units.
|
// 3. Install license file, if declared.
|
||||||
|
if let Some(license) = &pkg.license_file {
|
||||||
|
install_license(pkg, license)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Install desktop entry, if declared.
|
||||||
|
if let Some(desktop) = &pkg.desktop_file {
|
||||||
|
install_desktop_file(pkg, desktop)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Download + extract data archive, if declared.
|
||||||
|
if let Some(archive) = &pkg.data_archive {
|
||||||
|
install_data_archive(pkg, archive)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Install systemd user units.
|
||||||
let mut service_names = Vec::new();
|
let mut service_names = Vec::new();
|
||||||
for svc in &pkg.services {
|
for svc in &pkg.services {
|
||||||
install_service(svc, bin_dir, pkg)?;
|
install_service(svc, bin_dir, pkg)?;
|
||||||
service_names.push(svc.unit.clone());
|
service_names.push(svc.unit.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Run post_install hooks.
|
// 7. Run post_install hooks.
|
||||||
for cmd in &pkg.post_install {
|
for cmd in &pkg.post_install {
|
||||||
run_hook(cmd, &pkg.name)?;
|
run_hook(cmd, &pkg.name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Record in state.
|
// 8. Record in state.
|
||||||
let mut state = State::load()?;
|
let mut state = State::load()?;
|
||||||
state.record(InstalledPackage {
|
state.record(InstalledPackage {
|
||||||
name: pkg.name.clone(),
|
name: pkg.name.clone(),
|
||||||
|
|
@ -158,6 +173,110 @@ fn scaffold_config(cfg: &crate::manifest::ConfigScaffold, pkg: &Package) -> Resu
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download `filename` from `pkg`'s release dir, verify it against `sha256`
|
||||||
|
/// (refusing an unverified download the same way `scaffold_config` does),
|
||||||
|
/// and write it to `dest`. Shared by `install_license`/`install_desktop_file`
|
||||||
|
/// since both are "fetch one small artifact, verify, place" — unlike a config
|
||||||
|
/// example, these aren't user-editable, so they're always refreshed rather
|
||||||
|
/// than skipped when already present.
|
||||||
|
fn fetch_verify_write(
|
||||||
|
pkg: &Package,
|
||||||
|
filename: &str,
|
||||||
|
sha256: &Option<String>,
|
||||||
|
dest: &Path,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some((primary, fallback)) = pkg.artifact_urls(filename) else {
|
||||||
|
eprintln!(" warning: no artifact URL to download {label} ({filename})");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let bytes = match fetch_binary(&primary, &fallback) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" warning: could not download {label} {filename}: {e}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(expected) = sha256 else {
|
||||||
|
eprintln!(
|
||||||
|
" warning: index.json has no sha256 for {label} {filename} — \
|
||||||
|
refusing to install an unverified download"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if let Err(e) = verify_sha256(&bytes, expected) {
|
||||||
|
eprintln!(" warning: checksum mismatch for {label} {filename}: {e} — not installed");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(parent) = dest.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(dest, &bytes).with_context(|| format!("writing {}", dest.display()))?;
|
||||||
|
println!(" installed {label} at {}", dest.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_license(pkg: &Package, filename: &str) -> Result<()> {
|
||||||
|
let dest = dirs::data_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||||
|
.join("licenses")
|
||||||
|
.join(&pkg.name)
|
||||||
|
.join("LICENSE");
|
||||||
|
fetch_verify_write(pkg, filename, &pkg.license_file_sha256, &dest, "license")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_desktop_file(pkg: &Package, filename: &str) -> Result<()> {
|
||||||
|
let dest = dirs::data_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||||
|
.join("applications")
|
||||||
|
.join(format!("{}.desktop", pkg.name));
|
||||||
|
fetch_verify_write(pkg, filename, &pkg.desktop_file_sha256, &dest, "desktop entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_data_archive(pkg: &Package, filename: &str) -> Result<()> {
|
||||||
|
let data_dir = dirs::data_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||||
|
.join(&pkg.name);
|
||||||
|
fetch_extract_archive(pkg, filename, &pkg.data_archive_sha256, &data_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads + verifies a `.tar.gz` artifact, then extracts it into
|
||||||
|
/// `dest_dir`. Shells out to `tar` rather than adding an archive-extraction
|
||||||
|
/// crate dependency — `tar` is universally present on Linux and this file
|
||||||
|
/// already shells out to `systemctl` for the same "trust the base system
|
||||||
|
/// has this" reason. Split from `install_data_archive` (which just supplies
|
||||||
|
/// the real `~/.local/share/<name>` destination) so tests can extract into
|
||||||
|
/// a tempdir instead.
|
||||||
|
fn fetch_extract_archive(
|
||||||
|
pkg: &Package,
|
||||||
|
filename: &str,
|
||||||
|
sha256: &Option<String>,
|
||||||
|
dest_dir: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
let tmp_archive = std::env::temp_dir().join(format!("bakery-{}-{filename}", pkg.name));
|
||||||
|
|
||||||
|
fetch_verify_write(pkg, filename, sha256, &tmp_archive, "data archive")?;
|
||||||
|
if !tmp_archive.exists() {
|
||||||
|
// fetch_verify_write already warned (download/checksum failure).
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir_all(dest_dir)?;
|
||||||
|
let status = Command::new("tar")
|
||||||
|
.args(["xzf", &tmp_archive.to_string_lossy(), "-C"])
|
||||||
|
.arg(dest_dir)
|
||||||
|
.status()
|
||||||
|
.with_context(|| format!("running tar to extract {filename}"))?;
|
||||||
|
let _ = std::fs::remove_file(&tmp_archive);
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
println!(" extracted {filename} to {}", dest_dir.display());
|
||||||
|
} else {
|
||||||
|
eprintln!(" warning: tar exited with {status} extracting {filename}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn install_service(svc: &Service, bin_dir: &Path, pkg: &Package) -> Result<()> {
|
fn install_service(svc: &Service, bin_dir: &Path, pkg: &Package) -> Result<()> {
|
||||||
let service_dir = systemd_user_dir();
|
let service_dir = systemd_user_dir();
|
||||||
std::fs::create_dir_all(&service_dir)?;
|
std::fs::create_dir_all(&service_dir)?;
|
||||||
|
|
@ -343,9 +462,188 @@ fn warn_path_if_needed(bin_dir: &Path) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::manifest::{Binary, Package};
|
||||||
|
use sha2::Digest;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
/// Serves `body` for exactly one HTTP/1.0 request on an ephemeral local
|
||||||
|
/// port, then stops. Real network I/O over loopback — exercises
|
||||||
|
/// `fetch_verify_write`'s actual `fetch_binary` call, not just its
|
||||||
|
/// surrounding logic, without any new test dependency.
|
||||||
|
fn serve_once(body: &'static [u8]) -> String {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok((mut stream, _)) = listener.accept() {
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let _ = stream.read(&mut buf);
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.0 200 OK\r\nContent-Length: {}\r\n\r\n",
|
||||||
|
body.len()
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(response.as_bytes());
|
||||||
|
let _ = stream.write_all(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
format!("http://{addr}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `serve_once` but for a runtime-owned body (e.g. a tar.gz
|
||||||
|
/// built into a tempdir during the test), which can't satisfy `'static`.
|
||||||
|
fn serve_once_owned(body: Vec<u8>) -> String {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok((mut stream, _)) = listener.accept() {
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let _ = stream.read(&mut buf);
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.0 200 OK\r\nContent-Length: {}\r\n\r\n",
|
||||||
|
body.len()
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(response.as_bytes());
|
||||||
|
let _ = stream.write_all(&body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
format!("http://{addr}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_package(binary_url: &str) -> Package {
|
||||||
|
Package {
|
||||||
|
name: "breadhelp".to_string(),
|
||||||
|
description: "test".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
binaries: vec![Binary {
|
||||||
|
name: "breadhelp-x86_64".to_string(),
|
||||||
|
dl_url: format!("{binary_url}/breadhelp-x86_64"),
|
||||||
|
github_url: format!("{binary_url}/breadhelp-x86_64"),
|
||||||
|
sha256: String::new(),
|
||||||
|
}],
|
||||||
|
system_deps: vec![],
|
||||||
|
optional_system_deps: vec![],
|
||||||
|
bread_deps: vec![],
|
||||||
|
services: vec![],
|
||||||
|
config: None,
|
||||||
|
post_install: vec![],
|
||||||
|
license_file: None,
|
||||||
|
license_file_sha256: None,
|
||||||
|
desktop_file: None,
|
||||||
|
desktop_file_sha256: None,
|
||||||
|
data_archive: None,
|
||||||
|
data_archive_sha256: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_license_writes_verified_file() {
|
||||||
|
let license_bytes = b"MIT License\n";
|
||||||
|
let sha256 = sha2::Sha256::digest(license_bytes);
|
||||||
|
let sha256_hex = hex::encode(sha256);
|
||||||
|
|
||||||
|
let base_url = serve_once(license_bytes);
|
||||||
|
let mut pkg = test_package(&base_url);
|
||||||
|
pkg.license_file_sha256 = Some(sha256_hex);
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dest = dir.path().join("LICENSE");
|
||||||
|
fetch_verify_write(&pkg, "LICENSE", &pkg.license_file_sha256.clone(), &dest, "license")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read(&dest).unwrap(), license_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_desktop_file_writes_verified_file() {
|
||||||
|
let desktop_bytes = b"[Desktop Entry]\nName=BreadHelp\n";
|
||||||
|
let sha256 = sha2::Sha256::digest(desktop_bytes);
|
||||||
|
let sha256_hex = hex::encode(sha256);
|
||||||
|
|
||||||
|
let base_url = serve_once(desktop_bytes);
|
||||||
|
let mut pkg = test_package(&base_url);
|
||||||
|
pkg.desktop_file_sha256 = Some(sha256_hex);
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dest = dir.path().join("breadhelp.desktop");
|
||||||
|
fetch_verify_write(
|
||||||
|
&pkg,
|
||||||
|
"breadhelp.desktop",
|
||||||
|
&pkg.desktop_file_sha256.clone(),
|
||||||
|
&dest,
|
||||||
|
"desktop entry",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read(&dest).unwrap(), desktop_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_verify_write_refuses_checksum_mismatch() {
|
||||||
|
let bytes = b"tampered content";
|
||||||
|
let base_url = serve_once(bytes);
|
||||||
|
let mut pkg = test_package(&base_url);
|
||||||
|
pkg.license_file_sha256 = Some("0".repeat(64));
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dest = dir.path().join("LICENSE");
|
||||||
|
fetch_verify_write(&pkg, "LICENSE", &pkg.license_file_sha256.clone(), &dest, "license")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Refused, not erred (matches scaffold_config's warn-and-continue
|
||||||
|
// posture) — the file must not have been written.
|
||||||
|
assert!(!dest.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_verify_write_refuses_missing_sha256() {
|
||||||
|
let bytes = b"some content";
|
||||||
|
let base_url = serve_once(bytes);
|
||||||
|
let pkg = test_package(&base_url);
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dest = dir.path().join("LICENSE");
|
||||||
|
fetch_verify_write(&pkg, "LICENSE", &None, &dest, "license").unwrap();
|
||||||
|
|
||||||
|
assert!(!dest.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_extract_archive_extracts_tar_gz_contents() {
|
||||||
|
// Build a real tar.gz fixture via the actual `tar` binary — matches
|
||||||
|
// exactly what CI produces, rather than hand-rolling gzip framing.
|
||||||
|
let src = tempdir().unwrap();
|
||||||
|
fs::create_dir_all(src.path().join("content/tours")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
src.path().join("content/tours/onboarding.toml"),
|
||||||
|
b"[[step]]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let archive_path = src.path().join("content.tar.gz");
|
||||||
|
let status = Command::new("tar")
|
||||||
|
.args(["czf"])
|
||||||
|
.arg(&archive_path)
|
||||||
|
.args(["-C"])
|
||||||
|
.arg(src.path())
|
||||||
|
.arg("content")
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(status.success());
|
||||||
|
let archive_bytes = fs::read(&archive_path).unwrap();
|
||||||
|
|
||||||
|
let sha256_hex = hex::encode(sha2::Sha256::digest(&archive_bytes));
|
||||||
|
let base_url = serve_once_owned(archive_bytes);
|
||||||
|
let pkg = test_package(&base_url);
|
||||||
|
|
||||||
|
let dest_dir = tempdir().unwrap();
|
||||||
|
fetch_extract_archive(&pkg, "content.tar.gz", &Some(sha256_hex), dest_dir.path())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let extracted = dest_dir.path().join("content/tours/onboarding.toml");
|
||||||
|
assert_eq!(fs::read(&extracted).unwrap(), b"[[step]]\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strip_known_suffixes() {
|
fn strip_known_suffixes() {
|
||||||
assert_eq!(strip_arch_suffix("breadd-x86_64"), "breadd");
|
assert_eq!(strip_arch_suffix("breadd-x86_64"), "breadd");
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,30 @@ pub struct Package {
|
||||||
pub config: Option<ConfigScaffold>,
|
pub config: Option<ConfigScaffold>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub post_install: Vec<String>,
|
pub post_install: Vec<String>,
|
||||||
|
/// License artifact filename (e.g. "LICENSE"), installed to
|
||||||
|
/// `~/.local/share/licenses/<name>/LICENSE` — the bakery equivalent of
|
||||||
|
/// what a PKGBUILD's `package()` does with `/usr/share/licenses`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub license_file: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub license_file_sha256: Option<String>,
|
||||||
|
/// Desktop entry artifact filename (e.g. "breadhelp.desktop"),
|
||||||
|
/// installed to `~/.local/share/applications/<name>.desktop` so the
|
||||||
|
/// app shows up in any XDG-compliant launcher without root.
|
||||||
|
#[serde(default)]
|
||||||
|
pub desktop_file: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub desktop_file_sha256: Option<String>,
|
||||||
|
/// Data archive artifact filename (e.g. "content.tar.gz") — a `.tar.gz`
|
||||||
|
/// in the release dir, extracted to `~/.local/share/<name>/` on
|
||||||
|
/// install. For arbitrary data a package needs at runtime beyond a
|
||||||
|
/// config example (e.g. breadhelp's guide content), where a single
|
||||||
|
/// downloadable file + `tar` extraction is simpler than teaching
|
||||||
|
/// bakery to mirror a whole directory tree file-by-file.
|
||||||
|
#[serde(default)]
|
||||||
|
pub data_archive: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub data_archive_sha256: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Package {
|
impl Package {
|
||||||
|
|
@ -350,4 +374,41 @@ znmVfINB4jFDR2a4wuY8rOKlUBeSDOFjMkHYDXV3vxvAjK+r4V12ae9ZRQkfVtQ1YIEmFXbnJfbxywg+
|
||||||
assert_eq!(primary_url(Track::Beta), format!("{}/beta/index.json", base_url()));
|
assert_eq!(primary_url(Track::Beta), format!("{}/beta/index.json", base_url()));
|
||||||
assert_eq!(primary_url(Track::Dev), format!("{}/dev/index.json", base_url()));
|
assert_eq!(primary_url(Track::Dev), format!("{}/dev/index.json", base_url()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn minimal_package_json() -> &'static str {
|
||||||
|
r#"{
|
||||||
|
"name": "breadhelp",
|
||||||
|
"description": "test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"binaries": [],
|
||||||
|
"config": null
|
||||||
|
}"#
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn license_and_desktop_fields_default_to_none_on_old_shape_json() {
|
||||||
|
// Simulates an index.json produced before license_file/desktop_file
|
||||||
|
// existed — must not fail to parse.
|
||||||
|
let pkg: Package = serde_json::from_str(minimal_package_json()).unwrap();
|
||||||
|
assert!(pkg.license_file.is_none());
|
||||||
|
assert!(pkg.license_file_sha256.is_none());
|
||||||
|
assert!(pkg.desktop_file.is_none());
|
||||||
|
assert!(pkg.desktop_file_sha256.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn license_and_desktop_fields_roundtrip() {
|
||||||
|
let mut pkg: Package = serde_json::from_str(minimal_package_json()).unwrap();
|
||||||
|
pkg.license_file = Some("LICENSE".to_string());
|
||||||
|
pkg.license_file_sha256 = Some("abc123".to_string());
|
||||||
|
pkg.desktop_file = Some("breadhelp.desktop".to_string());
|
||||||
|
pkg.desktop_file_sha256 = Some("def456".to_string());
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&pkg).unwrap();
|
||||||
|
let restored: Package = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(restored.license_file.as_deref(), Some("LICENSE"));
|
||||||
|
assert_eq!(restored.license_file_sha256.as_deref(), Some("abc123"));
|
||||||
|
assert_eq!(restored.desktop_file.as_deref(), Some("breadhelp.desktop"));
|
||||||
|
assert_eq!(restored.desktop_file_sha256.as_deref(), Some("def456"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,8 @@ description = "Screenshot utility for the bread ecosystem"
|
||||||
name = "bos-settings"
|
name = "bos-settings"
|
||||||
repo = "Breadway/bos-settings"
|
repo = "Breadway/bos-settings"
|
||||||
description = "System settings app for Bread OS"
|
description = "System settings app for Bread OS"
|
||||||
|
|
||||||
|
[[products]]
|
||||||
|
name = "breadhelp"
|
||||||
|
repo = "Breadway/breadhelp"
|
||||||
|
description = "Onboarding and help center for Bread OS"
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,41 @@ build_package_json() {
|
||||||
local version
|
local version
|
||||||
version="$(basename "${version_dir}")"
|
version="$(basename "${version_dir}")"
|
||||||
|
|
||||||
|
# Locate bakery.toml. The release workflow copies it into the version dir
|
||||||
|
# alongside the binaries (${version_dir}/bakery.toml). Fall back to a
|
||||||
|
# sibling repo checkout for local dev use. Done before the binaries loop
|
||||||
|
# below so license_file/desktop_file (if declared) can be excluded from
|
||||||
|
# it by name — otherwise they'd get swept up as "binaries" with no
|
||||||
|
# checksum, the same gotcha this loop's other exclusions guard against.
|
||||||
|
local bakery_toml="${version_dir}/bakery.toml"
|
||||||
|
if [[ ! -f "${bakery_toml}" ]]; then
|
||||||
|
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "${bakery_toml}" ]]; then
|
||||||
|
echo "ERROR: bakery.toml not found for ${name} — the release workflow must copy it to \${PKG_ROOT}/${name}/\${VERSION}/bakery.toml" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local license_file_name desktop_file_name data_archive_name
|
||||||
|
license_file_name="$(python3 -c "
|
||||||
|
import tomllib
|
||||||
|
with open('${bakery_toml}', 'rb') as f:
|
||||||
|
d = tomllib.load(f)
|
||||||
|
print(d.get('license_file', ''))
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
desktop_file_name="$(python3 -c "
|
||||||
|
import tomllib
|
||||||
|
with open('${bakery_toml}', 'rb') as f:
|
||||||
|
d = tomllib.load(f)
|
||||||
|
print(d.get('desktop_file', ''))
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
data_archive_name="$(python3 -c "
|
||||||
|
import tomllib
|
||||||
|
with open('${bakery_toml}', 'rb') as f:
|
||||||
|
d = tomllib.load(f)
|
||||||
|
print(d.get('data_archive', ''))
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
|
||||||
# Collect all binaries in the version dir (executables only; skip metadata files).
|
# Collect all binaries in the version dir (executables only; skip metadata files).
|
||||||
local binaries_json="[]"
|
local binaries_json="[]"
|
||||||
for bin_path in "${version_dir}"/*; do
|
for bin_path in "${version_dir}"/*; do
|
||||||
|
|
@ -76,6 +111,9 @@ build_package_json() {
|
||||||
[[ "${bin_path}" == *.css ]] && continue
|
[[ "${bin_path}" == *.css ]] && continue
|
||||||
[[ "${bin_path}" == *.txt ]] && continue
|
[[ "${bin_path}" == *.txt ]] && continue
|
||||||
[[ "${bin_path}" == *.minisig ]] && continue
|
[[ "${bin_path}" == *.minisig ]] && continue
|
||||||
|
[[ -n "${license_file_name}" && "${bin_path}" == "${version_dir}/${license_file_name}" ]] && continue
|
||||||
|
[[ -n "${desktop_file_name}" && "${bin_path}" == "${version_dir}/${desktop_file_name}" ]] && continue
|
||||||
|
[[ -n "${data_archive_name}" && "${bin_path}" == "${version_dir}/${data_archive_name}" ]] && continue
|
||||||
[[ -f "${bin_path}" ]] || continue
|
[[ -f "${bin_path}" ]] || continue
|
||||||
local bin_name
|
local bin_name
|
||||||
bin_name="$(basename "${bin_path}")"
|
bin_name="$(basename "${bin_path}")"
|
||||||
|
|
@ -106,18 +144,6 @@ build_package_json() {
|
||||||
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Locate bakery.toml. The release workflow copies it into the version dir
|
|
||||||
# alongside the binaries (${version_dir}/bakery.toml). Fall back to a
|
|
||||||
# sibling repo checkout for local dev use.
|
|
||||||
local bakery_toml="${version_dir}/bakery.toml"
|
|
||||||
if [[ ! -f "${bakery_toml}" ]]; then
|
|
||||||
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
|
||||||
fi
|
|
||||||
if [[ ! -f "${bakery_toml}" ]]; then
|
|
||||||
echo "ERROR: bakery.toml not found for ${name} — the release workflow must copy it to \${PKG_ROOT}/${name}/\${VERSION}/bakery.toml" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local description system_deps optional_system_deps bread_deps services config post_install
|
local description system_deps optional_system_deps bread_deps services config post_install
|
||||||
|
|
||||||
description="$(python3 -c "
|
description="$(python3 -c "
|
||||||
|
|
@ -213,6 +239,47 @@ with open('${bakery_toml}', 'rb') as f:
|
||||||
print(json.dumps(d.get('install', {}).get('post_install', [])))
|
print(json.dumps(d.get('install', {}).get('post_install', [])))
|
||||||
" 2>/dev/null || echo "[]")"
|
" 2>/dev/null || echo "[]")"
|
||||||
|
|
||||||
|
# license_file / desktop_file: plain filename fields in bakery.toml
|
||||||
|
# (names already read above, before the binaries loop), same "artifact
|
||||||
|
# in the version dir, sha256 computed here" pattern as config.example.
|
||||||
|
# Empty string (not null) when unset, matching how the rest of this
|
||||||
|
# script signals "field absent" to jq below.
|
||||||
|
license_file="${license_file_name}"
|
||||||
|
license_file_sha256=""
|
||||||
|
if [[ -n "${license_file}" ]]; then
|
||||||
|
license_path="${version_dir}/${license_file}"
|
||||||
|
if [[ -f "${license_path}" ]]; then
|
||||||
|
license_file_sha256="$(sha256sum "${license_path}" | awk '{print $1}')"
|
||||||
|
else
|
||||||
|
echo " warning: license_file '${license_file}' not found at ${license_path}" >&2
|
||||||
|
license_file=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
desktop_file="${desktop_file_name}"
|
||||||
|
desktop_file_sha256=""
|
||||||
|
if [[ -n "${desktop_file}" ]]; then
|
||||||
|
desktop_path="${version_dir}/${desktop_file}"
|
||||||
|
if [[ -f "${desktop_path}" ]]; then
|
||||||
|
desktop_file_sha256="$(sha256sum "${desktop_path}" | awk '{print $1}')"
|
||||||
|
else
|
||||||
|
echo " warning: desktop_file '${desktop_file}' not found at ${desktop_path}" >&2
|
||||||
|
desktop_file=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
data_archive="${data_archive_name}"
|
||||||
|
data_archive_sha256=""
|
||||||
|
if [[ -n "${data_archive}" ]]; then
|
||||||
|
data_archive_path="${version_dir}/${data_archive}"
|
||||||
|
if [[ -f "${data_archive_path}" ]]; then
|
||||||
|
data_archive_sha256="$(sha256sum "${data_archive_path}" | awk '{print $1}')"
|
||||||
|
else
|
||||||
|
echo " warning: data_archive '${data_archive}' not found at ${data_archive_path}" >&2
|
||||||
|
data_archive=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
--arg name "${name}" \
|
--arg name "${name}" \
|
||||||
--arg description "${description}" \
|
--arg description "${description}" \
|
||||||
|
|
@ -224,6 +291,12 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
||||||
--argjson services "${services}" \
|
--argjson services "${services}" \
|
||||||
--argjson config "${config}" \
|
--argjson config "${config}" \
|
||||||
--argjson post_install "${post_install}" \
|
--argjson post_install "${post_install}" \
|
||||||
|
--arg license_file "${license_file}" \
|
||||||
|
--arg license_file_sha256 "${license_file_sha256}" \
|
||||||
|
--arg desktop_file "${desktop_file}" \
|
||||||
|
--arg desktop_file_sha256 "${desktop_file_sha256}" \
|
||||||
|
--arg data_archive "${data_archive}" \
|
||||||
|
--arg data_archive_sha256 "${data_archive_sha256}" \
|
||||||
'{
|
'{
|
||||||
name: $name,
|
name: $name,
|
||||||
description: $description,
|
description: $description,
|
||||||
|
|
@ -234,7 +307,13 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
||||||
bread_deps: $bread_deps,
|
bread_deps: $bread_deps,
|
||||||
services: $services,
|
services: $services,
|
||||||
config: $config,
|
config: $config,
|
||||||
post_install: $post_install
|
post_install: $post_install,
|
||||||
|
license_file: (if $license_file == "" then null else $license_file end),
|
||||||
|
license_file_sha256: (if $license_file_sha256 == "" then null else $license_file_sha256 end),
|
||||||
|
desktop_file: (if $desktop_file == "" then null else $desktop_file end),
|
||||||
|
desktop_file_sha256: (if $desktop_file_sha256 == "" then null else $desktop_file_sha256 end),
|
||||||
|
data_archive: (if $data_archive == "" then null else $data_archive end),
|
||||||
|
data_archive_sha256: (if $data_archive_sha256 == "" then null else $data_archive_sha256 end)
|
||||||
}'
|
}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue