launchpad: drive the endpoints from data/launchpad.yml

This commit is contained in:
2026-09-18 13:27:20 +02:00
parent ead97e1213
commit 7af767898e
5 changed files with 117 additions and 14 deletions
+26
View File
@@ -0,0 +1,26 @@
## Launchpad service endpoints: the REST API, the PPA SFTP upload queue,
## the PPA package-content host and the Ubuntu source-package git web UI.
## Like host_keys.yml, this file exists so that static endpoints are data,
## updatable in one reviewable place, instead of hardcoded in the source —
## the API and content URLs were previously triplicated across modules.
##
## Templates carry their variable parts as {name} placeholders ({owner},
## {ppa}, {package}), substituted by the accessors of src/launchpad.rs
## with plain string replacement.
##
## Where the values come from:
## api_base: the Launchpad REST API root (https://launchpad.net/docs/api/)
## ssh_*: the PPA upload queue, as expanded by dput-ng's
## ppa:user/ppa profile (ppa.launchpad.net:22, incoming
## ~<user>/<ppa>)
## content_host_template: ppa.launchpadcontent.net serves PPA apt
## repositories since the 2022 move off ppa.launchpad.net
## git_web_template: Launchpad's CGit mirrors of Ubuntu source packages
## (git.launchpad.net/ubuntu/+source/<package>)
api_base: https://api.launchpad.net/1.0
ssh_host: ppa.launchpad.net
ssh_port: 22
incoming_template: "~{owner}/{ppa}"
content_host_template: https://ppa.launchpadcontent.net/{owner}/{ppa}/ubuntu
git_web_template: https://git.launchpad.net/ubuntu/+source/{package}
+2 -2
View File
@@ -851,7 +851,7 @@ fn parse_ppa_url(ppa_base_url: &str) -> Option<(String, String)> {
.strip_prefix("https://")
.or_else(|| ppa_base_url.strip_prefix("http://"))?;
let (host, path) = rest.split_once('/')?;
if host != "ppa.launchpadcontent.net" {
if host != crate::launchpad::ppa_content_host() {
return None;
}
@@ -886,7 +886,7 @@ pub async fn ppa_keyring_bytes(
return Ok(cached.clone());
}
let api_url = format!("https://api.launchpad.net/1.0/~{owner}/+archive/ubuntu/{name}");
let api_url = crate::launchpad::archive_url(&owner, &name);
let response = crate::distro_info::http_get_retried(&api_url).await?;
if !response.status().is_success() {
return Err(format!(
+3 -2
View File
@@ -7,8 +7,9 @@
//!
//! This module is deliberately not a central registry: each file is
//! embedded by the module that owns it (distro_info.rs owns
//! data/distro_info.yml, put/ssh.rs owns data/host_keys.yml, quirks.rs
//! owns data/quirks.yml) through the [`embed_data!`] macro below, so data
//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml, put/ssh.rs
//! owns data/host_keys.yml, quirks.rs owns data/quirks.yml) through the
//! [`embed_data!`] macro below, so data
//! and its accessors stay together and a diff touching one domain cannot
//! half-touch another. The macro embeds the file at compile time and
//! parses it once into a `lazy_static` on first use; since the data ships
+84 -8
View File
@@ -23,13 +23,69 @@ use std::path::Path;
use serde::Deserialize;
use crate::data::embed_data;
use crate::put::target::UploadTarget;
/// Git configuration key holding the Launchpad account name
const LP_USER_KEY: &str = "lp.user";
/// Launchpad service endpoints, loaded from the bundled `launchpad.yml`
/// data file (same pattern as `distro_info.yml`): static endpoints that
/// change with Launchpad, not with the code, are data — several of them
/// were previously duplicated across three modules.
#[derive(Debug, Deserialize)]
struct LaunchpadData {
/// Base URL of the Launchpad REST API
api_base: String,
/// Host of the PPA SFTP upload server
ssh_host: String,
/// Port of the PPA SFTP upload server
ssh_port: u16,
/// Upload queue incoming directory template (`{owner}`/`{ppa}`)
incoming_template: String,
/// PPA package-content (apt repository) URL template
content_host_template: String,
/// Ubuntu source-package git web URL template (`{package}`)
git_web_template: String,
}
embed_data! {
static ref LAUNCHPAD_DATA: LaunchpadData = "../data/launchpad.yml"
}
/// Base URL of the Launchpad REST API
const API_BASE: &str = "https://api.launchpad.net/1.0";
fn api_base() -> &'static str {
&LAUNCHPAD_DATA.api_base
}
/// Host serving PPA package content, derived from the content-host
/// template so the URL builders and the URL parsers of PPA addresses
/// cannot drift apart
pub(crate) fn ppa_content_host() -> &'static str {
let template = LAUNCHPAD_DATA.content_host_template.as_str();
let after_scheme = template
.split_once("://")
.map_or(template, |(_, rest)| rest);
after_scheme.split('/').next().unwrap_or(after_scheme)
}
/// Base URL of the apt repository serving a PPA's packages
/// (e.g. `https://ppa.launchpadcontent.net/user/ppa/ubuntu`)
pub(crate) fn ppa_content_url(owner: &str, ppa: &str) -> String {
LAUNCHPAD_DATA
.content_host_template
.replace("{owner}", owner)
.replace("{ppa}", ppa)
}
/// URL of the Launchpad git repository of an Ubuntu source package
/// (`git.launchpad.net/ubuntu/+source/<package>`), the preferred VCS of
/// Ubuntu packages
pub(crate) fn ubuntu_source_git_url(package: &str) -> String {
LAUNCHPAD_DATA
.git_web_template
.replace("{package}", package)
}
/// Page size (`ws.size`) asked from Launchpad collections. Launchpad
/// truncates collection answers at 75 entries by default and rejects
@@ -88,27 +144,30 @@ fn split_ppa(ppa: &str) -> Result<(String, String), String> {
/// URL of the Launchpad API resource of a Launchpad account
fn person_url(user: &str) -> String {
format!("{API_BASE}/~{user}")
format!("{}/~{user}", api_base())
}
/// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name`
/// covers the default `ppa` archive and named archives alike); shared by the
/// put-side pre-flight checks and the apt keyring's fingerprint lookup
pub(crate) fn archive_url(user: &str, ppa: &str) -> String {
format!("{API_BASE}/~{user}/+archive/ubuntu/{ppa}")
format!("{}/~{user}/+archive/ubuntu/{ppa}", api_base())
}
/// Resolve a `user/ppa_name` PPA argument into its upload target
/// (`ppa.launchpad.net`, incoming `~user/ppa_name`), like dput-ng's
/// Resolve a `user/ppa_name` PPA argument into its upload target (the
/// SFTP host and incoming template of `launchpad.yml`), like dput-ng's
/// `ppa:user/ppa` profile expansion.
pub fn ppa_target(ppa: &str) -> Result<UploadTarget, String> {
let (user, name) = split_ppa(ppa)?;
Ok(UploadTarget {
fqdn: "ppa.launchpad.net".to_string(),
port: 22,
fqdn: LAUNCHPAD_DATA.ssh_host.clone(),
port: LAUNCHPAD_DATA.ssh_port,
login: None,
incoming: format!("~{user}/{name}"),
incoming: LAUNCHPAD_DATA
.incoming_template
.replace("{owner}", &user)
.replace("{ppa}", &name),
label: format!("ppa:{ppa}"),
})
}
@@ -425,6 +484,23 @@ mod tests {
);
}
/// The data-driven endpoint accessors build the same addresses the
/// former hardcoded constants carried (each verified against the live
/// service), and the content host is derived from the same template
/// the content URLs are built from
#[test]
fn data_driven_endpoints_match_the_service() {
assert_eq!(
ppa_content_url("vhaudiquet", "noctalia"),
"https://ppa.launchpadcontent.net/vhaudiquet/noctalia/ubuntu"
);
assert_eq!(ppa_content_host(), "ppa.launchpadcontent.net");
assert_eq!(
ubuntu_source_git_url("hello"),
"https://git.launchpad.net/ubuntu/+source/hello"
);
}
/// The API answer carries many unrelated fields; deserialization must
/// pick the relevant ones and tolerate a null `enabled`
#[test]
+2 -2
View File
@@ -18,11 +18,11 @@ use log::{debug, warn};
/// # Returns
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/")
pub fn ppa_to_base_url(user: &str, name: &str) -> String {
format!("https://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name)
crate::launchpad::ppa_content_url(user, name)
}
fn check_launchpad_repo_sync(package: &str) -> Result<Option<String>, String> {
let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package);
let url = crate::launchpad::ubuntu_source_git_url(package);
// Use libgit2 to check if the remote repository exists
// This is more reliable than HTTP HEAD requests when CGIt is disabled