distro_info: add UNRELEASED series handling, use it in deb and chlog

This commit is contained in:
2026-09-16 11:15:09 +02:00
parent 4af8dbddb0
commit ae420989f9
3 changed files with 116 additions and 5 deletions
+12
View File
@@ -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
};
+74
View File
@@ -169,6 +169,16 @@ pub fn supported_dists() -> Vec<String> {
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<Vec<SeriesInformation>, Box<dyn Error>> {
let dist_data = DATA.dist.get(dist).ok_or_else(|| {
@@ -206,6 +216,24 @@ pub async fn get_ordered_series_name(dist: &str) -> Result<Vec<String>, Box<dyn
Ok(series.iter().map(|info| info.series.clone()).collect())
}
/// The series to actually target when the changelog says [`UNRELEASED`]:
/// the development series of `dist`, i.e. the first entry of
/// [`get_ordered_series_name`] (which is documented "development series
/// first"). UNRELEASED work conventionally targets the next release, not
/// the last stable one. Any other `series` is returned unchanged. Errors
/// when `dist` is unknown or has no series list.
pub async fn effective_series(series: &str, dist: &str) -> Result<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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();
+30 -5
View File
@@ -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(&current_series).await?;
pkh::distro_info::get_ordered_series_name(&dist).await
if pkh::distro_info::is_unreleased(&current_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(&current_series).await?;
pkh::distro_info::get_ordered_series_name(&dist).await
}
}) {
Ok(series_list) => {
match pkh::ui::select_series(&series_list, &current_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(&current_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!(