From 225157be636cd02f1c5eb9686e3edda43c78212f Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Thu, 29 Jan 2026 17:11:01 +0100 Subject: [PATCH] pull: allow pulling from ppa --- README.md | 2 +- src/deb/mod.rs | 7 +++--- src/main.rs | 17 ++++++++++---- src/package_info.rs | 56 +++++++++++++++++++++++++++++++++++++++------ src/pull.rs | 2 +- 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d056a48..ebfe68f 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Missing features: - [ ] `pkh pull` - [x] Obtain package sources from git - [x] Obtain package sources from the archive (fallback) - - [ ] Obtain package source from PPA (--ppa) + - [x] Obtain package source from PPA (--ppa) - [ ] Obtain a specific version of the package - [x] Fetch the correct git branch for series on Ubuntu - [ ] Try to fetch the correct git branch for series on Debian, or fallback to the archive diff --git a/src/deb/mod.rs b/src/deb/mod.rs index b24415c..e6dea62 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -212,9 +212,10 @@ mod tests { log::debug!("Created temporary directory: {}", cwd.display()); log::info!("Pulling package {} from {}...", package, series); - let package_info = crate::package_info::lookup(package, None, Some(series), "", dist, None) - .await - .expect("Cannot lookup package information"); + let package_info = + crate::package_info::lookup(package, None, Some(series), "", dist, None, None) + .await + .expect("Cannot lookup package information"); crate::pull::pull(&package_info, Some(cwd), None, true) .await .expect("Cannot pull package"); diff --git a/src/main.rs b/src/main.rs index 118bcd5..1e475fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,14 +94,22 @@ fn main() { let series = sub_matches.get_one::("series").map(|s| s.as_str()); let dist = sub_matches.get_one::("dist").map(|s| s.as_str()); let version = sub_matches.get_one::("version").map(|s| s.as_str()); - let _ppa = sub_matches - .get_one::("ppa") - .map(|s| s.as_str()) - .unwrap_or(""); + let ppa = sub_matches.get_one::("ppa").map(|s| s.as_str()); let archive = sub_matches.get_one::("archive").unwrap_or(&false); let (pb, progress_callback) = ui::create_progress_bar(&multi); + // Convert PPA to base URL if provided + let base_url = ppa.and_then(|ppa_str| { + // PPA format: user/ppa_name + let parts: Vec<&str> = ppa_str.split('/').collect(); + if parts.len() == 2 { + Some(pkh::package_info::ppa_to_base_url(parts[0], parts[1])) + } else { + None + } + }); + // Since pull is async, we need to block on it if let Err(e) = rt.block_on(async { let package_info = pkh::package_info::lookup( @@ -110,6 +118,7 @@ fn main() { series, "", dist, + base_url.as_deref(), Some(&progress_callback), ) .await?; diff --git a/src/package_info.rs b/src/package_info.rs index 8843876..c0c9113 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -6,6 +6,18 @@ use std::io::Read; use crate::ProgressCallback; use log::{debug, warn}; +/// Convert a PPA specification to a base URL +/// +/// # Arguments +/// * user: user for the PPA +/// * name: name of the PPA +/// +/// # 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) +} + async fn check_launchpad_repo(package: &str) -> Result, Box> { let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package); let client = reqwest::Client::builder() @@ -178,6 +190,7 @@ async fn get( series: &str, pocket: &str, version: Option<&str>, + base_url: Option<&str>, ) -> Result> { let dist = crate::distro_info::get_dist_from_series(series).await?; @@ -191,7 +204,16 @@ async fn get( preferred_vcs = Some(lp_url); } - let base_url = crate::distro_info::get_base_url(&dist); + // Determine the base URL to use (either provided PPA URL or default archive) + let distro_base_url = crate::distro_info::get_base_url(&dist); + let base_url = if let Some(ppa_url) = base_url { + ppa_url.to_string() + } else { + distro_base_url.clone() + }; + + // If using a custom base URL (PPA), disable VCS lookup to force archive download + let from_ppa = base_url != distro_base_url; let components = crate::distro_info::get_components(&base_url, series, pocket).await?; debug!("Found components: {:?}", components); @@ -228,6 +250,11 @@ async fn get( preferred_vcs = Some(vcs.clone()); } + // If downloading from PPA, make sure we don't use a VCS + if from_ppa { + preferred_vcs = None; + } + let archive_url = format!("{base_url}/{0}", stanza.directory); return Ok(PackageInfo { dist, @@ -252,6 +279,7 @@ async fn find_package( dist: &str, pocket: &str, version: Option<&str>, + base_url: Option<&str>, progress: ProgressCallback<'_>, ) -> Result> { let series_list = crate::distro_info::get_ordered_series_name(dist).await?; @@ -268,7 +296,7 @@ async fn find_package( }; for p in pockets { - match get(package_name, series, &p, version).await { + match get(package_name, series, &p, version, base_url).await { Ok(info) => { if i > 0 { warn!( @@ -297,12 +325,22 @@ async fn find_package( /// /// This function obtains package information either directly from a specific series /// or by searching across all series in a distribution. +/// +/// # Arguments +/// * `package` - The name of the package to look up +/// * `version` - Optional specific version to look for +/// * `series` - Optional distribution series (e.g., "noble", "bookworm") +/// * `pocket` - Pocket to search in (e.g., "updates", "security", or "" for main) +/// * `dist` - Optional distribution name (e.g., "ubuntu", "debian") +/// * `base_url` - Optional base URL for the package archive (e.g., "https://ppa.launchpadcontent.net/user/ppa/ubuntu/") +/// * `progress` - Optional progress callback pub async fn lookup( package: &str, version: Option<&str>, series: Option<&str>, pocket: &str, dist: Option<&str>, + base_url: Option<&str>, progress: ProgressCallback<'_>, ) -> Result> { // Obtain the package information, either directly in a series or with a search in all series @@ -317,7 +355,7 @@ pub async fn lookup( } // Get the package information from that series and pocket - get(package, s, pocket, version).await? + get(package, s, pocket, version, base_url).await? } else { let dist = dist.unwrap_or_else(|| // Use auto-detection to see if current distro is ubuntu, or fallback to debian by default @@ -331,7 +369,11 @@ pub async fn lookup( if let Some(cb) = progress { cb( - &format!("Searching for package {} in {}...", package, dist), + &format!( + "Searching for package {} in {}...", + package, + if base_url.is_none() { dist } else { "ppa" } + ), "", 0, 0, @@ -339,7 +381,7 @@ pub async fn lookup( } // Try to find the package in all series from that dist - find_package(package, dist, pocket, version, progress).await? + find_package(package, dist, pocket, version, base_url, progress).await? }; Ok(package_info) @@ -403,7 +445,7 @@ Version: 1.0 #[tokio::test] async fn test_find_package_fallback() { // python2.7 is in bullseye but not above - let info = find_package("python2.7", "debian", "", None, None) + let info = find_package("python2.7", "debian", "", None, None, None) .await .unwrap(); assert_eq!(info.stanza.package, "python2.7"); @@ -413,7 +455,7 @@ Version: 1.0 #[tokio::test] async fn test_find_package_devel() { // hello is in sid - let info = find_package("hello", "debian", "", None, None) + let info = find_package("hello", "debian", "", None, None, None) .await .unwrap(); assert_eq!(info.stanza.package, "hello"); diff --git a/src/pull.rs b/src/pull.rs index 604e0b2..9c8aa71 100644 --- a/src/pull.rs +++ b/src/pull.rs @@ -590,7 +590,7 @@ mod tests { let cwd = temp_dir.path(); // Main 'pull' command: the one we want to test - let info = crate::package_info::lookup(package, None, series, "", dist, None) + let info = crate::package_info::lookup(package, None, series, "", dist, None, None) .await .unwrap(); pull(&info, Some(cwd), None, archive.unwrap_or(false))