Compare commits
2
Commits
3b99ece39a
...
b34e86dcfe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34e86dcfe | ||
|
|
5500f98586 |
@@ -1492,6 +1492,7 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
Some(dist),
|
Some(dist),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
.unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e));
|
.unwrap_or_else(|e| panic!("package lookup failed for {}: {}", package, e));
|
||||||
rt.block_on(crate::pull::pull(
|
rt.block_on(crate::pull::pull(
|
||||||
|
|||||||
+22
-9
@@ -276,18 +276,30 @@ pub async fn build(
|
|||||||
return Err("Could not install build-dependencies for the build".into());
|
return Err("Could not install build-dependencies for the build".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install arch-independant build dependencies
|
// Install arch-independant build dependencies, only if the source declares
|
||||||
|
// any: without --arch-only this pass resolves the whole Build-Depends field
|
||||||
|
// too, which is redundant after the first pass and breaks cross builds.
|
||||||
|
let has_indep_deps = match ctx.read_file(&package_dir.join("debian/control")) {
|
||||||
|
Ok(control) => control
|
||||||
|
.lines()
|
||||||
|
.any(|l| l.to_ascii_lowercase().starts_with("build-depends-indep:")),
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("cannot read debian/control for indep build-deps: {}", e);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if has_indep_deps {
|
||||||
log::debug!("Installing arch-independant build dependencies...");
|
log::debug!("Installing arch-independant build dependencies...");
|
||||||
let status = cap(
|
let mut cmd = ctx.command("apt-get");
|
||||||
ctx.command("apt-get")
|
cmd.current_dir(package_dir_str)
|
||||||
.current_dir(package_dir_str)
|
|
||||||
.envs(env.clone())
|
.envs(env.clone())
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
.arg("build-dep")
|
.arg("build-dep");
|
||||||
.arg("./"),
|
if cross {
|
||||||
&sink,
|
cmd.arg(format!("--host-architecture={arch}"));
|
||||||
)
|
}
|
||||||
.status()?;
|
cmd.arg("./");
|
||||||
|
let status = cap(&mut cmd, &sink).status()?;
|
||||||
|
|
||||||
// If build-dep fails, we try to explain the failure using dose-debcheck
|
// If build-dep fails, we try to explain the failure using dose-debcheck
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
@@ -297,6 +309,7 @@ pub async fn build(
|
|||||||
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
|
||||||
return Err("Could not install build-dependencies for the build".into());
|
return Err("Could not install build-dependencies for the build".into());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run the build step
|
// Run the build step
|
||||||
log::debug!("Building (debian/rules build) package...");
|
log::debug!("Building (debian/rules build) package...");
|
||||||
|
|||||||
+120
-1
@@ -383,7 +383,7 @@ mod tests {
|
|||||||
|
|
||||||
log::info!("Pulling package {} from {}...", package, series);
|
log::info!("Pulling package {} from {}...", package, series);
|
||||||
let package_info =
|
let package_info =
|
||||||
crate::package_info::lookup(package, None, Some(series), "", dist, None, None)
|
crate::package_info::lookup(package, None, Some(series), "", dist, None, None, None)
|
||||||
.await
|
.await
|
||||||
.expect("Cannot lookup package information");
|
.expect("Cannot lookup package information");
|
||||||
crate::pull::pull(&package_info, Some(cwd), None, true)
|
crate::pull::pull(&package_info, Some(cwd), None, true)
|
||||||
@@ -492,4 +492,123 @@ mod tests {
|
|||||||
async fn test_deb_gcc_debian_end_to_end() {
|
async fn test_deb_gcc_debian_end_to_end() {
|
||||||
test_build_end_to_end("gcc-15", "sid", None, None, false).await;
|
test_build_end_to_end("gcc-15", "sid", None, None, false).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a synthetic source package that discriminates which architecture
|
||||||
|
/// is used to resolve Build-Depends-Indep during cross builds:
|
||||||
|
///
|
||||||
|
/// - 'libdb-dev' is an arch:any package that is not Multi-Arch: same, so an
|
||||||
|
/// amd64 copy can only be installed by replacing the arm64 one
|
||||||
|
/// - the arch-specific binary links against libdb for the host
|
||||||
|
/// architecture, so the build only succeeds if the arm64 libdb-dev was
|
||||||
|
/// left in place by the arch-independant build-dep pass
|
||||||
|
fn create_indep_cross_test_source(parent: &Path) -> PathBuf {
|
||||||
|
let pkg_dir = parent.join("pkh-crosstest");
|
||||||
|
std::fs::create_dir_all(pkg_dir.join("debian/source")).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/changelog"),
|
||||||
|
"pkh-crosstest (1.0) noble; urgency=medium\n\n \
|
||||||
|
* Synthetic package exercising Build-Depends-Indep in cross builds.\n\n \
|
||||||
|
-- pkh tests <pkh@example.com> Tue, 15 Sep 2026 08:00:00 +0000\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/control"),
|
||||||
|
"Source: pkh-crosstest\n\
|
||||||
|
Section: devel\n\
|
||||||
|
Priority: optional\n\
|
||||||
|
Maintainer: pkh tests <pkh@example.com>\n\
|
||||||
|
Standards-Version: 4.7.4\n\
|
||||||
|
Build-Depends: debhelper-compat (= 13), libdb-dev\n\
|
||||||
|
Build-Depends-Indep: libdb-dev\n\
|
||||||
|
Architecture: any all\n\
|
||||||
|
\n\
|
||||||
|
Package: pkh-crosstest\n\
|
||||||
|
Architecture: any\n\
|
||||||
|
Depends: ${misc:Depends}, ${shlibs:Depends}\n\
|
||||||
|
Description: Cross-build regression package for build-dep resolution\n \
|
||||||
|
Builds a host-architecture binary against libdb to detect a cross\n \
|
||||||
|
build environment damaged by a wrongly-scoped build-dep pass.\n\
|
||||||
|
\n\
|
||||||
|
Package: pkh-crosstest-data\n\
|
||||||
|
Architecture: all\n\
|
||||||
|
Description: Cross-build regression package data (arch-indep)\n \
|
||||||
|
Arch-indep binary so the indep build path is exercised.\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/rules"),
|
||||||
|
"#!/usr/bin/make -f\n\
|
||||||
|
%:\n\
|
||||||
|
\tdh $@\n\
|
||||||
|
\n\
|
||||||
|
override_dh_auto_build:\n\
|
||||||
|
\tprintf '#include <db.h>\\nint main(void){DB *d; return db_create(&d, NULL, 0);}\\n' > main.c\n\
|
||||||
|
\t$(DEB_HOST_GNU_TYPE)-gcc main.c -ldb -o pkh-crosstest\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let rules = pkg_dir.join("debian/rules");
|
||||||
|
let mut perms = std::fs::metadata(&rules).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
std::fs::set_permissions(&rules, perms).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(pkg_dir.join("debian/source/format"), "3.0 (native)\n").unwrap();
|
||||||
|
|
||||||
|
pkg_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This ensures the arch-independant build-dep pass of a cross build
|
||||||
|
/// resolves dependencies for the host architecture, like dpkg-checkbuilddeps
|
||||||
|
/// does, instead of re-resolving the whole Build-Depends field for the
|
||||||
|
/// native architecture, which swaps host-arch -dev packages for native ones
|
||||||
|
/// and breaks the cross build environment.
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
async fn test_deb_cross_indep_host_arch_end_to_end() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let pkg_dir = create_indep_cross_test_source(temp_dir.path());
|
||||||
|
|
||||||
|
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local));
|
||||||
|
|
||||||
|
crate::deb::build_binary_package(
|
||||||
|
Some("arm64"),
|
||||||
|
Some("noble"),
|
||||||
|
None,
|
||||||
|
Some(&pkg_dir),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(ctx),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Cannot cross-build package declaring Build-Depends-Indep");
|
||||||
|
|
||||||
|
// Both binary packages must have been produced, including the
|
||||||
|
// arch-independant one
|
||||||
|
let deb_files: Vec<String> = std::fs::read_dir(temp_dir.path())
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
deb_files
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.starts_with("pkh-crosstest_1.0_arm64.deb")),
|
||||||
|
"arch-specific .deb not produced, got: {deb_files:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
deb_files
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.starts_with("pkh-crosstest-data_1.0_all.deb")),
|
||||||
|
"arch-independant .deb not produced, got: {deb_files:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ fn main() {
|
|||||||
.arg(arg!(-v --version <version> "Target package version").required(false))
|
.arg(arg!(-v --version <version> "Target package version").required(false))
|
||||||
.arg(arg!(--archive "Only use the archive to download package source, not git").required(false))
|
.arg(arg!(--archive "Only use the archive to download package source, not git").required(false))
|
||||||
.arg(arg!(--ppa <ppa> "Download the package from a specific PPA").required(false))
|
.arg(arg!(--ppa <ppa> "Download the package from a specific PPA").required(false))
|
||||||
|
.arg(arg!(--repository <url> "Download the package from an external flat repository, given as its full suite URL (e.g. https://pkg.noctalia.dev/deb/resolute/)").required(false)
|
||||||
|
.conflicts_with("ppa"))
|
||||||
.arg(arg!(-p --pocket <pocket> "Target package distribution pocket (updates, security, proposed)").required(false))
|
.arg(arg!(-p --pocket <pocket> "Target package distribution pocket (updates, security, proposed)").required(false))
|
||||||
.arg(arg!(<package> "Target package")),
|
.arg(arg!(<package> "Target package")),
|
||||||
)
|
)
|
||||||
@@ -138,6 +140,9 @@ fn main() {
|
|||||||
let dist = sub_matches.get_one::<String>("dist").map(|s| s.as_str());
|
let dist = sub_matches.get_one::<String>("dist").map(|s| s.as_str());
|
||||||
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str());
|
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str());
|
||||||
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
|
||||||
|
let repository = sub_matches
|
||||||
|
.get_one::<String>("repository")
|
||||||
|
.map(|s| s.as_str());
|
||||||
let pocket = sub_matches
|
let pocket = sub_matches
|
||||||
.get_one::<String>("pocket")
|
.get_one::<String>("pocket")
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
@@ -166,6 +171,7 @@ fn main() {
|
|||||||
pocket,
|
pocket,
|
||||||
dist,
|
dist,
|
||||||
base_url.as_deref(),
|
base_url.as_deref(),
|
||||||
|
repository,
|
||||||
Some(&progress_callback),
|
Some(&progress_callback),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
+169
-4
@@ -2,6 +2,7 @@ use flate2::read::GzDecoder;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use xz2::read::XzDecoder;
|
||||||
|
|
||||||
use crate::ProgressCallback;
|
use crate::ProgressCallback;
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
@@ -151,15 +152,29 @@ impl PackageInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decompress a package index payload, detecting the compression format from
|
||||||
|
/// its magic bytes: gzip ('1f 8b'), xz ('fd 7zXZ 00'), or plain text
|
||||||
|
fn decompress_index(data: &[u8]) -> Result<String, Box<dyn Error>> {
|
||||||
|
let mut decoded: Box<dyn Read> = if data.starts_with(&[0x1f, 0x8b]) {
|
||||||
|
Box::new(GzDecoder::new(data))
|
||||||
|
} else if data.starts_with(&[0xfd, 0x37, b'z', b'X', b'Z', 0x00]) {
|
||||||
|
Box::new(XzDecoder::new(data))
|
||||||
|
} else {
|
||||||
|
return Ok(String::from_utf8(data.to_vec())?);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut s = String::new();
|
||||||
|
decoded.read_to_string(&mut s)?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
struct DebianSources {
|
struct DebianSources {
|
||||||
splitted_sources: std::str::Split<'static, &'static str>,
|
splitted_sources: std::str::Split<'static, &'static str>,
|
||||||
}
|
}
|
||||||
impl DebianSources {
|
impl DebianSources {
|
||||||
fn new(data: &[u8]) -> Result<DebianSources, Box<dyn Error>> {
|
fn new(data: &[u8]) -> Result<DebianSources, Box<dyn Error>> {
|
||||||
// Gz-decode 'Sources.gz' file into a string, and split it on stanzas
|
// Decode the index into a string, and split it on stanzas
|
||||||
let mut d = GzDecoder::new(data);
|
let s = decompress_index(data)?;
|
||||||
let mut s = String::new();
|
|
||||||
d.read_to_string(&mut s)?;
|
|
||||||
|
|
||||||
// Convert the string to a static lifetime by leaking it
|
// Convert the string to a static lifetime by leaking it
|
||||||
let static_str = Box::leak(s.into_boxed_str());
|
let static_str = Box::leak(s.into_boxed_str());
|
||||||
@@ -423,6 +438,123 @@ async fn find_package(
|
|||||||
Err(format!("Package '{}' not found.", package_name).into())
|
Err(format!("Package '{}' not found.", package_name).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the 'Release' file at the root of a flat repository, and return its suite name
|
||||||
|
///
|
||||||
|
/// Flat repositories (e.g. 'https://pkg.example.org/deb/resolute/') serve the
|
||||||
|
/// Release file at their root, without any 'dists/' hierarchy. The suite is
|
||||||
|
/// read from the 'Codename' field, falling back to 'Suite'.
|
||||||
|
async fn get_flat_repo_series(base_url: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
let url = format!("{}/Release", base_url.trim_end_matches('/'));
|
||||||
|
let response = reqwest::get(&url).await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!(
|
||||||
|
"No Release file at '{}' (HTTP {}) - is '{}' the suite URL of a flat repository? \
|
||||||
|
External repositories must be passed as their full suite URL.",
|
||||||
|
url,
|
||||||
|
response.status(),
|
||||||
|
base_url
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
let content = response.text().await?;
|
||||||
|
|
||||||
|
for field in ["Codename", "Suite"] {
|
||||||
|
let prefix = format!("{field}:");
|
||||||
|
if let Some(line) = content.lines().find(|l| l.starts_with(&prefix)) {
|
||||||
|
return Ok(line[prefix.len()..].trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("No Codename or Suite field in Release file at '{url}'").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the sources index of a flat repository
|
||||||
|
///
|
||||||
|
/// Flat repositories are free to serve any compressed variant of the index
|
||||||
|
/// (or an uncompressed one), so try the usual candidates in turn.
|
||||||
|
async fn get_flat_repo_sources(base_url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
let base = base_url.trim_end_matches('/');
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for name in ["Sources.xz", "Sources.gz", "Sources"] {
|
||||||
|
let url = format!("{base}/{name}");
|
||||||
|
match reqwest::get(&url).await {
|
||||||
|
Ok(response) if response.status().is_success() => {
|
||||||
|
return Ok(response.bytes().await?.to_vec());
|
||||||
|
}
|
||||||
|
Ok(response) => errors.push(format!("{}: HTTP {}", url, response.status())),
|
||||||
|
Err(e) => errors.push(format!("{}: {}", url, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"Could not fetch the sources index from '{}': {}",
|
||||||
|
base,
|
||||||
|
errors.join(", ")
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup package information in an external flat repository
|
||||||
|
///
|
||||||
|
/// The URL points at the repository suite directory itself (e.g.
|
||||||
|
/// 'https://pkg.noctalia.dev/deb/resolute/'), as flat repositories have no
|
||||||
|
/// 'dists/<suite>' hierarchy. The suite name is read from the 'Release' file
|
||||||
|
/// unless overridden, and the package files are fetched relative to the URL
|
||||||
|
/// root, ignoring the 'Directory' field like apt does for flat repositories.
|
||||||
|
///
|
||||||
|
/// Packages from external repositories are always downloaded from the
|
||||||
|
/// repository itself; the 'Vcs-Git' of the stanza is never used, as it may
|
||||||
|
/// point to an arbitrary source.
|
||||||
|
pub async fn lookup_repository(
|
||||||
|
package: &str,
|
||||||
|
version: Option<&str>,
|
||||||
|
repo_url: &str,
|
||||||
|
series: Option<&str>,
|
||||||
|
progress: ProgressCallback<'_>,
|
||||||
|
) -> Result<PackageInfo, Box<dyn Error>> {
|
||||||
|
if let Some(cb) = progress {
|
||||||
|
cb(
|
||||||
|
&format!("Fetching repository info from {}...", repo_url),
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved_series = if let Some(s) = series {
|
||||||
|
s.to_string()
|
||||||
|
} else {
|
||||||
|
get_flat_repo_series(repo_url).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cb) = progress {
|
||||||
|
cb(
|
||||||
|
&format!("Resolving package info for {}...", package),
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sources = get_flat_repo_sources(repo_url).await?;
|
||||||
|
let stanza = parse_sources(&sources, package, version)?
|
||||||
|
.ok_or_else(|| format!("Package '{package}' not found in repository {repo_url}"))?;
|
||||||
|
|
||||||
|
// The distribution is only used for naming (e.g. git branch layout);
|
||||||
|
// fall back to a neutral name for suites unknown to distro-info.
|
||||||
|
let dist = crate::distro_info::get_dist_from_series(&resolved_series)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "external".to_string());
|
||||||
|
|
||||||
|
Ok(PackageInfo {
|
||||||
|
dist,
|
||||||
|
series: resolved_series,
|
||||||
|
stanza,
|
||||||
|
preferred_vcs: None,
|
||||||
|
archive_url: repo_url.trim_end_matches('/').to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Lookup package information for a source package
|
/// Lookup package information for a source package
|
||||||
///
|
///
|
||||||
/// This function obtains package information either directly from a specific series
|
/// This function obtains package information either directly from a specific series
|
||||||
@@ -435,7 +567,9 @@ async fn find_package(
|
|||||||
/// * `pocket` - Pocket to search in (e.g., "updates", "security", or "" for main)
|
/// * `pocket` - Pocket to search in (e.g., "updates", "security", or "" for main)
|
||||||
/// * `dist` - Optional distribution name (e.g., "ubuntu", "debian")
|
/// * `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/")
|
/// * `base_url` - Optional base URL for the package archive (e.g., "https://ppa.launchpadcontent.net/user/ppa/ubuntu/")
|
||||||
|
/// * `repository` - Optional URL of an external flat repository suite (e.g., "https://pkg.noctalia.dev/deb/resolute/")
|
||||||
/// * `progress` - Optional progress callback
|
/// * `progress` - Optional progress callback
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn lookup(
|
pub async fn lookup(
|
||||||
package: &str,
|
package: &str,
|
||||||
version: Option<&str>,
|
version: Option<&str>,
|
||||||
@@ -443,8 +577,15 @@ pub async fn lookup(
|
|||||||
pocket: &str,
|
pocket: &str,
|
||||||
dist: Option<&str>,
|
dist: Option<&str>,
|
||||||
base_url: Option<&str>,
|
base_url: Option<&str>,
|
||||||
|
repository: Option<&str>,
|
||||||
progress: ProgressCallback<'_>,
|
progress: ProgressCallback<'_>,
|
||||||
) -> Result<PackageInfo, Box<dyn Error>> {
|
) -> Result<PackageInfo, Box<dyn Error>> {
|
||||||
|
// External flat repository: the URL is the suite root, distribution
|
||||||
|
// resolution and pocket logic do not apply
|
||||||
|
if let Some(repo_url) = repository {
|
||||||
|
return lookup_repository(package, version, repo_url, series, progress).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve the distribution early so we can check Launchpad once
|
// Resolve the distribution early so we can check Launchpad once
|
||||||
let resolved_dist = dist.unwrap_or_else(||
|
let resolved_dist = dist.unwrap_or_else(||
|
||||||
// Use auto-detection to see if current distro is ubuntu, or fallback to debian by default
|
// Use auto-detection to see if current distro is ubuntu, or fallback to debian by default
|
||||||
@@ -582,6 +723,30 @@ Version: 1.0
|
|||||||
assert!(none.is_none());
|
assert!(none.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_sources_compression_variants() {
|
||||||
|
use std::io::Write;
|
||||||
|
use xz2::write::XzEncoder;
|
||||||
|
|
||||||
|
let data = "Package: hello
|
||||||
|
Version: 1.0
|
||||||
|
Directory: pool/main/h/hello
|
||||||
|
";
|
||||||
|
|
||||||
|
// xz-compressed index (e.g. the 'Sources.xz' of flat repositories)
|
||||||
|
let mut encoder = XzEncoder::new(Vec::new(), 6);
|
||||||
|
encoder.write_all(data.as_bytes()).unwrap();
|
||||||
|
let compressed = encoder.finish().unwrap();
|
||||||
|
let info = parse_sources(&compressed, "hello", None).unwrap().unwrap();
|
||||||
|
assert_eq!(info.version, "1.0");
|
||||||
|
|
||||||
|
// Uncompressed index
|
||||||
|
let info = parse_sources(data.as_bytes(), "hello", None)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(info.version, "1.0");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_find_package_fallback() {
|
async fn test_find_package_fallback() {
|
||||||
// python2.7 is in bullseye but not above
|
// python2.7 is in bullseye but not above
|
||||||
|
|||||||
+41
-6
@@ -153,13 +153,23 @@ fn copy_file_times(src: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of extracting an archive
|
||||||
|
struct ExtractedArchive {
|
||||||
|
/// Paths of the extracted files
|
||||||
|
files: Vec<String>,
|
||||||
|
/// The archive used './'-prefixed entries: its contents were extracted
|
||||||
|
/// directly into the destination directory, and there is no
|
||||||
|
/// 'package-version/' top-level directory to relocate
|
||||||
|
in_place: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function to extract tar archive with progress tracking
|
/// Helper function to extract tar archive with progress tracking
|
||||||
fn extract_tar_archive<D, F>(
|
fn extract_tar_archive<D, F>(
|
||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
progress: ProgressCallback<'_>,
|
progress: ProgressCallback<'_>,
|
||||||
decoder_factory: F,
|
decoder_factory: F,
|
||||||
) -> Result<Vec<String>, Box<dyn Error>>
|
) -> Result<ExtractedArchive, Box<dyn Error>>
|
||||||
where
|
where
|
||||||
D: std::io::Read,
|
D: std::io::Read,
|
||||||
F: Fn(File) -> D,
|
F: Fn(File) -> D,
|
||||||
@@ -178,11 +188,28 @@ where
|
|||||||
let mut archive = Archive::new(decoder);
|
let mut archive = Archive::new(decoder);
|
||||||
|
|
||||||
let mut extracted_files = Vec::new();
|
let mut extracted_files = Vec::new();
|
||||||
|
let mut in_place = false;
|
||||||
|
|
||||||
for entry in archive.entries()? {
|
for entry in archive.entries()? {
|
||||||
let mut entry = entry?;
|
let mut entry = entry?;
|
||||||
let path = entry.path()?.to_path_buf();
|
let path = entry.path()?.to_path_buf();
|
||||||
let dest_path = dest.join(&path);
|
|
||||||
|
// Archives built with './'-prefixed entries (common in third-party
|
||||||
|
// repositories) target the destination directory itself, with no
|
||||||
|
// 'package-version/' wrapper; skip their root entry, and remember
|
||||||
|
// the layout for the caller
|
||||||
|
let relative = if path.to_string_lossy().starts_with("./") {
|
||||||
|
in_place = true;
|
||||||
|
path.strip_prefix("./").unwrap_or(&path).to_path_buf()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
if relative.as_os_str().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dest_path = dest.join(&relative);
|
||||||
|
|
||||||
// Create parent directories if needed
|
// Create parent directories if needed
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
@@ -201,14 +228,17 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(extracted_files)
|
Ok(ExtractedArchive {
|
||||||
|
files: extracted_files,
|
||||||
|
in_place,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_archive(
|
fn extract_archive(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
progress: ProgressCallback<'_>,
|
progress: ProgressCallback<'_>,
|
||||||
) -> Result<Vec<String>, Box<dyn Error>> {
|
) -> Result<ExtractedArchive, Box<dyn Error>> {
|
||||||
let filename = path.file_name().unwrap().to_string_lossy();
|
let filename = path.file_name().unwrap().to_string_lossy();
|
||||||
|
|
||||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||||
@@ -465,10 +495,14 @@ async fn fetch_archive_sources(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archives with './'-prefixed entries are already laid out
|
||||||
|
// directly in the package directory; only 'package-version/'
|
||||||
|
// style ones need their contents relocated
|
||||||
|
if !extracted.in_place {
|
||||||
// List root directories extracted and use the first one as the source directory
|
// List root directories extracted and use the first one as the source directory
|
||||||
debug!("Root directories extracted:");
|
debug!("Root directories extracted:");
|
||||||
let mut source_dir: Option<PathBuf> = None;
|
let mut source_dir: Option<PathBuf> = None;
|
||||||
for file in &extracted {
|
for file in &extracted.files {
|
||||||
let path = Path::new(file);
|
let path = Path::new(file);
|
||||||
// Check if this is a directory and is at the archive root level
|
// Check if this is a directory and is at the archive root level
|
||||||
// (i.e., the path relative to extract_dir has no parent components)
|
// (i.e., the path relative to extract_dir has no parent components)
|
||||||
@@ -509,6 +543,7 @@ async fn fetch_archive_sources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extract and apply .diff.gz if present (old packages)
|
// Extract and apply .diff.gz if present (old packages)
|
||||||
if file.name.ends_with(".diff.gz") {
|
if file.name.ends_with(".diff.gz") {
|
||||||
@@ -679,7 +714,7 @@ mod tests {
|
|||||||
let cwd = temp_dir.path();
|
let cwd = temp_dir.path();
|
||||||
|
|
||||||
// Main 'pull' command: the one we want to test
|
// Main 'pull' command: the one we want to test
|
||||||
let info = crate::package_info::lookup(package, None, series, "", dist, None, None)
|
let info = crate::package_info::lookup(package, None, series, "", dist, None, None, None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pull(&info, Some(cwd), None, archive.unwrap_or(false))
|
pull(&info, Some(cwd), None, archive.unwrap_or(false))
|
||||||
|
|||||||
Reference in New Issue
Block a user