bakery: add stable/beta/dev build tracks
Adds a track concept to bakery (separate from the existing bakery/pacman distribution channel): stable (unchanged tag-triggered releases), beta (deliberate beta-v* tag promotion), and dev (published on every push to dev). Each track gets its own signed index + artifact tree under dl.breadway.dev so stable's paths and existing installs are untouched. - bakery: new Track type, a global track preference in installed.json (defaults to stable via serde, no migration needed), `bakery track show`/`set`, a BAKERY_INDEX_BASE_URL override for testing, and a real semver comparison in `update` (was a plain string-equality check before). ANSI-colored/aligned CLI output (TTY + NO_COLOR aware). - gen-index.sh: TRACK env var selects which subtree to read/write. - CI: dev-bakery.yml/beta-bakery.yml/dev-bread-theme.yml/beta-bread-theme.yml publish those two products on the new tracks; dev/beta skip the GitHub Release upload step (no per-commit release spam). - docs/release-channels.md documents the three-track policy.
This commit is contained in:
parent
db2fa3c4b4
commit
4ac54c610d
15 changed files with 771 additions and 67 deletions
|
|
@ -3,11 +3,14 @@ mod download;
|
|||
mod install;
|
||||
mod manifest;
|
||||
mod state;
|
||||
mod track;
|
||||
mod ui;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use track::Track;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "bakery", about = "Package manager for the bread ecosystem", version)]
|
||||
|
|
@ -54,6 +57,20 @@ enum Cmd {
|
|||
/// Package to check; omit to check all installed packages
|
||||
package: Option<String>,
|
||||
},
|
||||
/// View or switch which build track bakery follows (stable/beta/dev)
|
||||
Track {
|
||||
#[command(subcommand)]
|
||||
action: TrackCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TrackCmd {
|
||||
/// Show the currently selected track
|
||||
Show,
|
||||
/// Switch tracks. Only changes the preference — run `bakery update --all`
|
||||
/// afterwards to actually install builds from the new track.
|
||||
Set { track: Track },
|
||||
}
|
||||
|
||||
fn default_bin_dir() -> PathBuf {
|
||||
|
|
@ -65,23 +82,52 @@ fn default_bin_dir() -> PathBuf {
|
|||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let bin_dir = cli.bin_dir.unwrap_or_else(default_bin_dir);
|
||||
let track = state::State::load()?.track;
|
||||
|
||||
match cli.command {
|
||||
Cmd::Install { packages } => {
|
||||
let index = manifest::load(true)?;
|
||||
let index = manifest::load(true, track)?;
|
||||
for pkg in &packages {
|
||||
cmd_install(&index, pkg, &bin_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Remove { package } => cmd_remove(&package, &bin_dir),
|
||||
Cmd::Update { package, all } => cmd_update(package.as_deref(), all, &bin_dir),
|
||||
Cmd::List { installed } => cmd_list(installed),
|
||||
Cmd::Info { package } => cmd_info(&package),
|
||||
Cmd::Doctor { package } => cmd_doctor(package.as_deref()),
|
||||
Cmd::Update { package, all } => cmd_update(package.as_deref(), all, &bin_dir, track),
|
||||
Cmd::List { installed } => cmd_list(installed, track),
|
||||
Cmd::Info { package } => cmd_info(&package, track),
|
||||
Cmd::Doctor { package } => cmd_doctor(package.as_deref(), track),
|
||||
Cmd::Track { action } => cmd_track(action),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_track(action: TrackCmd) -> Result<()> {
|
||||
let mut state = state::State::load()?;
|
||||
match action {
|
||||
TrackCmd::Show => {
|
||||
println!("current track: {}", ui::style(state.track.as_str(), ui::CYAN));
|
||||
}
|
||||
TrackCmd::Set { track } => {
|
||||
if state.track == track {
|
||||
println!("already on track {track}");
|
||||
return Ok(());
|
||||
}
|
||||
// Fail fast on a bad/unreachable track rather than silently
|
||||
// recording a preference bakery can't actually serve.
|
||||
manifest::load(true, track)
|
||||
.with_context(|| format!("could not validate {track} track, not switching"))?;
|
||||
state.set_track(track);
|
||||
state.save()?;
|
||||
println!(
|
||||
"switched to {} — run 'bakery update --all' to install {} builds",
|
||||
ui::style(track.as_str(), ui::CYAN),
|
||||
track
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_install(index: &manifest::Index, name: &str, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let mut visited = HashSet::new();
|
||||
install_with_deps(index, name, bin_dir, &mut visited)
|
||||
|
|
@ -130,8 +176,8 @@ fn cmd_remove(name: &str, bin_dir: &std::path::Path) -> Result<()> {
|
|||
install::remove_package(name, bin_dir)
|
||||
}
|
||||
|
||||
fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let index = manifest::load(true)?;
|
||||
fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path, track: Track) -> Result<()> {
|
||||
let index = manifest::load(true, track)?;
|
||||
let state = state::State::load()?;
|
||||
|
||||
let targets: Vec<String> = if all || name.is_none() {
|
||||
|
|
@ -162,14 +208,16 @@ fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Resul
|
|||
}
|
||||
};
|
||||
|
||||
if installed.version == latest.version {
|
||||
println!("{pkg_name} is already at {}", installed.version);
|
||||
if !is_newer(&installed.version, &latest.version) {
|
||||
println!("{}", ui::style(&format!("{pkg_name} is already at {}", installed.version), ui::GREEN));
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"updating {pkg_name} {} → {}",
|
||||
installed.version, latest.version
|
||||
"updating {pkg_name} {} {} {}",
|
||||
ui::style(&installed.version, ui::DIM),
|
||||
ui::style("→", ui::CYAN),
|
||||
ui::style(&latest.version, ui::BOLD)
|
||||
);
|
||||
|
||||
let rep = match doctor::check_deps(&latest.system_deps, &latest.optional_system_deps) {
|
||||
|
|
@ -204,7 +252,28 @@ fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Resul
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_list(installed_only: bool) -> Result<()> {
|
||||
/// Is `latest` newer than `installed`? Real semver comparison — the
|
||||
/// previous plain string-equality check couldn't tell "different" from
|
||||
/// "actually newer", so it would happily "update" a package to a lexically
|
||||
/// different but not-newer version. Falls back to a simple inequality check
|
||||
/// (with a warning) for any version string that isn't valid semver, rather
|
||||
/// than hard-erroring on packages built before this convention existed.
|
||||
fn is_newer(installed: &str, latest: &str) -> bool {
|
||||
match (semver::Version::parse(installed), semver::Version::parse(latest)) {
|
||||
(Ok(i), Ok(l)) => l > i,
|
||||
_ => {
|
||||
if installed != latest {
|
||||
eprintln!(
|
||||
" warning: '{installed}' or '{latest}' is not valid semver, \
|
||||
falling back to a plain inequality check"
|
||||
);
|
||||
}
|
||||
installed != latest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_list(installed_only: bool, track: Track) -> Result<()> {
|
||||
let state = state::State::load()?;
|
||||
|
||||
if installed_only {
|
||||
|
|
@ -217,35 +286,39 @@ fn cmd_list(installed_only: bool) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let index = manifest::load(false)?;
|
||||
if !matches!(track, Track::Stable) {
|
||||
println!("tracking:{}\n", ui::track_badge(track));
|
||||
}
|
||||
|
||||
let index = manifest::load(false, track)?;
|
||||
let mut names: Vec<&str> = index.packages.keys().map(|s| s.as_str()).collect();
|
||||
names.sort();
|
||||
for name in names {
|
||||
let pkg = &index.packages[name];
|
||||
let tag = if state.is_installed(name) {
|
||||
format!(" [installed {}]", state.packages[name].version)
|
||||
ui::style(&format!(" [installed {}]", state.packages[name].version), ui::GREEN)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!(" {} {} — {}{}", pkg.name, pkg.version, pkg.description, tag);
|
||||
println!(" {:<14} {:<10} — {}{}", pkg.name, pkg.version, pkg.description, tag);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_info(name: &str) -> Result<()> {
|
||||
let index = manifest::load(false)?;
|
||||
fn cmd_info(name: &str, track: Track) -> Result<()> {
|
||||
let index = manifest::load(false, track)?;
|
||||
let pkg = index
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown package: {name}"))?;
|
||||
|
||||
let state = state::State::load()?;
|
||||
let status = if let Some(inst) = state.packages.get(name) {
|
||||
format!("installed ({})", inst.version)
|
||||
ui::style(&format!("installed ({})", inst.version), ui::GREEN)
|
||||
} else {
|
||||
"not installed".to_string()
|
||||
ui::style("not installed", ui::DIM)
|
||||
};
|
||||
|
||||
println!("{} {}", pkg.name, pkg.version);
|
||||
println!("{}{} {}", ui::style(&pkg.name, ui::BOLD), ui::track_badge(track), pkg.version);
|
||||
println!(" {}", pkg.description);
|
||||
println!(" status: {status}");
|
||||
println!(
|
||||
|
|
@ -278,8 +351,8 @@ fn cmd_info(name: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_doctor(name: Option<&str>) -> Result<()> {
|
||||
let index = manifest::load(false)?;
|
||||
fn cmd_doctor(name: Option<&str>, track: Track) -> Result<()> {
|
||||
let index = manifest::load(false, track)?;
|
||||
let state = state::State::load()?;
|
||||
|
||||
let targets: Vec<String> = match name {
|
||||
|
|
@ -310,7 +383,41 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> {
|
|||
}
|
||||
|
||||
if all_ok {
|
||||
println!("all checks passed");
|
||||
println!("{}", ui::ok("all checks passed"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_newer_detects_real_semver_increase() {
|
||||
assert!(is_newer("0.3.1", "0.3.2"));
|
||||
assert!(is_newer("0.3.1", "0.4.0"));
|
||||
assert!(!is_newer("0.3.2", "0.3.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_newer_false_when_equal() {
|
||||
assert!(!is_newer("0.3.1", "0.3.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_newer_orders_dev_prereleases_within_a_track() {
|
||||
// Two dev builds of the same upcoming patch, ordered by their
|
||||
// timestamp+sha build suffix.
|
||||
assert!(is_newer(
|
||||
"0.3.2-dev.20260722120000+aaa1111",
|
||||
"0.3.2-dev.20260722130000+bbb2222"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_newer_falls_back_to_inequality_on_unparseable_versions() {
|
||||
// Pre-semver version strings should never hard-fail an update check.
|
||||
assert!(is_newer("weird-version-1", "weird-version-2"));
|
||||
assert!(!is_newer("weird-version-1", "weird-version-1"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue