diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 0dfc160..c051a8b 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -83,8 +83,20 @@ async fn build_binary_package_impl( let changelog_path = cwd.join("debian/changelog"); let (package, version, package_series) = crate::changelog::parse_changelog_header(&changelog_path)?; + // UNRELEASED is not a real archive series: without an explicit --series, + // build against the development series of the host vendor's distribution + // instead. An explicit --series always wins. + let resolved_series; let series = if let Some(s) = series { s + } else if crate::distro_info::is_unreleased(&package_series) { + let dist = crate::build::env::current_vendor(); + resolved_series = crate::distro_info::effective_series(&package_series, &dist).await?; + log::info!( + "Changelog is UNRELEASED, building against series {}", + resolved_series + ); + resolved_series.as_str() } else { &package_series }; diff --git a/src/distro_info.rs b/src/distro_info.rs index 845e490..94b88e3 100644 --- a/src/distro_info.rs +++ b/src/distro_info.rs @@ -169,6 +169,16 @@ pub fn supported_dists() -> Vec { DATA.dist.keys().cloned().collect() } +/// Special changelog distribution marking an entry that has not been +/// released to any archive series yet +pub const UNRELEASED: &str = "UNRELEASED"; + +/// Whether `series` is the special [`UNRELEASED`] distribution rather than +/// a real archive series +pub fn is_unreleased(series: &str) -> bool { + series == UNRELEASED +} + /// Get time-ordered list of series information for a distribution, development series first pub async fn get_ordered_series(dist: &str) -> Result, Box> { let dist_data = DATA.dist.get(dist).ok_or_else(|| { @@ -206,6 +216,24 @@ pub async fn get_ordered_series_name(dist: &str) -> Result, Box Result> { + if !is_unreleased(series) { + return Ok(series.to_string()); + } + + get_ordered_series_name(dist) + .await? + .into_iter() + .next() + .ok_or_else(|| format!("Distribution '{}' has no series to target", dist).into()) +} + /// Get the latest released series for a dist (excluding future releases and special cases like sid) pub async fn get_latest_released_series(dist: &str) -> Result> { let latest = get_n_latest_released_series(dist, 1).await?; @@ -523,6 +551,52 @@ mod tests { ); } + #[test] + fn test_is_unreleased() { + // Matching is exact: UNRELEASED is uppercase by Debian convention + assert!(is_unreleased("UNRELEASED")); + assert!(!is_unreleased("unreleased")); + assert!(!is_unreleased("noble")); + assert!(!is_unreleased("")); + } + + #[tokio::test] + async fn test_effective_series_passthrough() { + // A real series is returned unchanged, and the dist is not even + // looked up (an unknown dist only matters for UNRELEASED) + assert_eq!(effective_series("noble", "ubuntu").await.unwrap(), "noble"); + assert_eq!(effective_series("sid", "debian").await.unwrap(), "sid"); + assert_eq!( + effective_series("noble", "unknown-distro").await.unwrap(), + "noble" + ); + } + + #[tokio::test] + async fn test_effective_series_unreleased() { + // UNRELEASED resolves to the development series of the dist, i.e. + // the first entry of the time-ordered list. On current distro-info + // data this is the next Ubuntu release, while Debian's list starts + // with 'experimental' (sid comes second), so assert against the + // data itself rather than a hardcoded name. + for dist in ["ubuntu", "debian"] { + let ordered = get_ordered_series_name(dist).await.unwrap(); + let resolved = effective_series(UNRELEASED, dist).await.unwrap(); + assert_eq!(resolved, ordered[0]); + assert_ne!(resolved, UNRELEASED); + } + } + + #[tokio::test] + async fn test_effective_series_unknown_dist() { + // UNRELEASED on an unknown distribution cannot be resolved + assert!( + effective_series(UNRELEASED, "unknown-distro") + .await + .is_err() + ); + } + #[tokio::test] async fn test_get_debian_series() { let series = get_ordered_series_name("debian").await.unwrap(); diff --git a/src/main.rs b/src/main.rs index 2a68a73..6c245b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,14 +198,39 @@ fn main() { let changelog_path = cwd.join("debian/changelog"); match pkh::changelog::parse_changelog_header(&changelog_path) { Ok((_pkg, _ver, current_series)) => { - // Try to get the list of available series for this distribution + // UNRELEASED is not a real series: offer it as a + // pinned first entry (selecting it keeps the changelog + // unreleased) on top of the current vendor's series + // list, defaulting to the development series. Any + // other series resolves through the series list of + // its own distribution. match rt.block_on(async { - let dist = - pkh::distro_info::get_dist_from_series(¤t_series).await?; - pkh::distro_info::get_ordered_series_name(&dist).await + if pkh::distro_info::is_unreleased(¤t_series) { + let dist = pkh::build::env::current_vendor(); + let mut series_list = + vec![pkh::distro_info::UNRELEASED.to_string()]; + series_list.extend( + pkh::distro_info::get_ordered_series_name(&dist).await?, + ); + Ok(series_list) + } else { + let dist = + pkh::distro_info::get_dist_from_series(¤t_series).await?; + pkh::distro_info::get_ordered_series_name(&dist).await + } }) { Ok(series_list) => { - match pkh::ui::select_series(&series_list, ¤t_series) { + // Default to the development series (the + // first real entry) when the changelog is + // UNRELEASED, not to the pinned entry itself + let default = if pkh::distro_info::is_unreleased(¤t_series) + && series_list.len() > 1 + { + series_list[1].clone() + } else { + current_series.clone() + }; + match pkh::ui::select_series(&series_list, &default) { Ok(selected) => Some(selected), Err(e) => { error!(