feat: update package version to 0.6.0 and enhance binary package handling
This commit is contained in:
Generated
+1
-1
@@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "Depot")]
|
#[command(name = "Depot")]
|
||||||
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
|
#[command(about = "Source-based package manager for Linux", long_about = None)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Custom root filesystem path
|
/// Custom root filesystem path
|
||||||
|
|||||||
+457
-122
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
signing, source, staging, ui,
|
signing, source, staging, ui,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -35,6 +36,205 @@ fn parse_dependency_list(metadata: &toml::Value, kind: &str) -> Vec<String> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_package_archive_into_staging(
|
||||||
|
archive_path: &Path,
|
||||||
|
) -> Result<(package::PackageSpec, tempfile::TempDir)> {
|
||||||
|
let tmp_dir = tempfile::TempDir::new().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to create staging dir for {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let extract_dir = tmp_dir.path().to_path_buf();
|
||||||
|
|
||||||
|
let file = fs::File::open(archive_path)
|
||||||
|
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
|
||||||
|
let zstd_decoder = zstd::stream::read::Decoder::new(file)
|
||||||
|
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
|
||||||
|
let mut archive = tar::Archive::new(zstd_decoder);
|
||||||
|
|
||||||
|
let mut metadata_content = String::new();
|
||||||
|
for entry in archive.entries().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read archive entries from {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})? {
|
||||||
|
let mut entry = entry.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read archive entry from {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if entry.path()?.to_string_lossy() == ".metadata.toml" {
|
||||||
|
use std::io::Read;
|
||||||
|
entry
|
||||||
|
.read_to_string(&mut metadata_content)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read .metadata.toml in {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata_content.is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Package archive does not contain .metadata.toml: {}",
|
||||||
|
archive_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = fs::File::open(archive_path)
|
||||||
|
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
|
||||||
|
let zstd_decoder = zstd::stream::read::Decoder::new(file)
|
||||||
|
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
|
||||||
|
let mut archive = tar::Archive::new(zstd_decoder);
|
||||||
|
archive.unpack(&extract_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to extract package archive {} into {}",
|
||||||
|
archive_path.display(),
|
||||||
|
extract_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let metadata: toml::Value = toml::from_str(&metadata_content).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to parse .metadata.toml in {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: metadata
|
||||||
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
version: metadata
|
||||||
|
.get("version")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
revision: metadata
|
||||||
|
.get("revision")
|
||||||
|
.and_then(|v| v.as_integer())
|
||||||
|
.unwrap_or(1) as u32,
|
||||||
|
description: metadata
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
homepage: metadata
|
||||||
|
.get("homepage")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
license: parse_licenses_from_toml(&metadata),
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Bin,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: parse_dependency_list(&metadata, "runtime"),
|
||||||
|
test: Vec::new(),
|
||||||
|
optional: parse_dependency_list(&metadata, "optional"),
|
||||||
|
},
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(provides) = metadata.get("provides").and_then(|v| v.as_array()) {
|
||||||
|
spec.alternatives.provides = provides
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((spec, tmp_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_package_archive_to_staging(archive_path: &Path) -> Result<tempfile::TempDir> {
|
||||||
|
let tmp_dir = tempfile::TempDir::new().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to create staging dir for {}",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let extract_dir = tmp_dir.path().to_path_buf();
|
||||||
|
|
||||||
|
let file = fs::File::open(archive_path)
|
||||||
|
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
|
||||||
|
let zstd_decoder = zstd::stream::read::Decoder::new(file)
|
||||||
|
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
|
||||||
|
let mut archive = tar::Archive::new(zstd_decoder);
|
||||||
|
archive.unpack(&extract_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to extract package archive {} into {}",
|
||||||
|
archive_path.display(),
|
||||||
|
extract_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(tmp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_license_list_from_repo(license: &Option<String>) -> Vec<String> {
|
||||||
|
let Some(raw) = license.as_ref() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
raw.split(',')
|
||||||
|
.map(|part| part.trim())
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package_spec_from_repo_record(
|
||||||
|
record: &db::repo::BinaryRepoPackageRecord,
|
||||||
|
) -> package::PackageSpec {
|
||||||
|
package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: record.name.clone(),
|
||||||
|
version: record.version.clone(),
|
||||||
|
revision: record.revision,
|
||||||
|
description: record.description.clone().unwrap_or_default(),
|
||||||
|
homepage: record.homepage.clone().unwrap_or_default(),
|
||||||
|
license: parse_license_list_from_repo(&record.license),
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives {
|
||||||
|
provides: record.provides.clone(),
|
||||||
|
replaces: Vec::new(),
|
||||||
|
},
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Bin,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: record.runtime_dependencies.clone(),
|
||||||
|
test: Vec::new(),
|
||||||
|
optional: record.optional_dependencies.clone(),
|
||||||
|
},
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_type_runs_automatic_tests(build_type: package::BuildType) -> bool {
|
fn build_type_runs_automatic_tests(build_type: package::BuildType) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
build_type,
|
build_type,
|
||||||
@@ -405,6 +605,26 @@ fn install_staged_to_rootfs(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn install_package_outputs_to_rootfs(
|
||||||
|
pkg_spec: &package::PackageSpec,
|
||||||
|
destdir: &Path,
|
||||||
|
rootfs: &Path,
|
||||||
|
config: &config::Config,
|
||||||
|
) -> Result<Vec<package::PackageInfo>> {
|
||||||
|
let mut installed = Vec::new();
|
||||||
|
for out in pkg_spec.outputs() {
|
||||||
|
let mut spec_for_out = pkg_spec.clone();
|
||||||
|
let output_name = out.name.clone();
|
||||||
|
spec_for_out.package = out;
|
||||||
|
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||||
|
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||||
|
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
|
||||||
|
install_staged_to_rootfs(&spec_for_out, &out_destdir, rootfs, config)?;
|
||||||
|
installed.push(spec_for_out.package);
|
||||||
|
}
|
||||||
|
Ok(installed)
|
||||||
|
}
|
||||||
|
|
||||||
fn repo_kind_label(kind: RepoKindArg) -> &'static str {
|
fn repo_kind_label(kind: RepoKindArg) -> &'static str {
|
||||||
match kind {
|
match kind {
|
||||||
RepoKindArg::Source => "source",
|
RepoKindArg::Source => "source",
|
||||||
@@ -711,6 +931,9 @@ fn print_plan_summary(plan: &planner::ExecutionPlan) {
|
|||||||
summary.skipped_installed,
|
summary.skipped_installed,
|
||||||
human_bytes(summary.known_download_bytes)
|
human_bytes(summary.known_download_bytes)
|
||||||
));
|
));
|
||||||
|
if std::env::var_os("DEPOT_VERBOSE_PLAN").is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for step in &plan.steps {
|
for step in &plan.steps {
|
||||||
let (action, origin) = match &step.action {
|
let (action, origin) = match &step.action {
|
||||||
planner::PlanAction::SkipInstalled => ("skip", "installed".to_string()),
|
planner::PlanAction::SkipInstalled => ("skip", "installed".to_string()),
|
||||||
@@ -755,8 +978,15 @@ fn execute_install_plan_with_child_commands(
|
|||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct BinaryPhaseItem {
|
||||||
|
repo_name: String,
|
||||||
|
record: db::repo::BinaryRepoPackageRecord,
|
||||||
|
}
|
||||||
|
|
||||||
let summary = plan.summary();
|
let summary = plan.summary();
|
||||||
if plan.actionable_steps().next().is_none() {
|
let actionable_steps: Vec<_> = plan.actionable_steps().collect();
|
||||||
|
if actionable_steps.is_empty() {
|
||||||
ui::info("Nothing to do.");
|
ui::info("Nothing to do.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -767,8 +997,8 @@ fn execute_install_plan_with_child_commands(
|
|||||||
summary.source_build_installs
|
summary.source_build_installs
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let planned_packages: Vec<String> = plan
|
let planned_packages: Vec<String> = actionable_steps
|
||||||
.actionable_steps()
|
.iter()
|
||||||
.map(|step| step.package.clone())
|
.map(|step| step.package.clone())
|
||||||
.collect();
|
.collect();
|
||||||
if !ui::prompt_package_action("installation", &planned_packages, true)? {
|
if !ui::prompt_package_action("installation", &planned_packages, true)? {
|
||||||
@@ -780,31 +1010,133 @@ fn execute_install_plan_with_child_commands(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
|
let mut binary_phase_items = Vec::new();
|
||||||
|
for step in &actionable_steps {
|
||||||
|
if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin {
|
||||||
|
binary_phase_items.push(BinaryPhaseItem {
|
||||||
|
repo_name: repo_name.clone(),
|
||||||
|
record: (**record).clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for step in plan.actionable_steps() {
|
let mut binary_archives: HashMap<(String, String), db::repo::BinaryRepoCachedArchive> =
|
||||||
let input_path = match &step.origin {
|
HashMap::new();
|
||||||
planner::PlanOrigin::Source { path, .. } => path.clone(),
|
if !binary_phase_items.is_empty() {
|
||||||
planner::PlanOrigin::Binary { repo_name, record } => {
|
ui::info(format!(
|
||||||
|
"Downloading {} binary package(s) and detached signatures...",
|
||||||
|
binary_phase_items.len()
|
||||||
|
));
|
||||||
|
for (idx, item) in binary_phase_items.iter().enumerate() {
|
||||||
|
ui::info(format!(
|
||||||
|
" [{}/{}] {}-{} ({})",
|
||||||
|
idx + 1,
|
||||||
|
binary_phase_items.len(),
|
||||||
|
item.record.name,
|
||||||
|
item.record.version,
|
||||||
|
item.repo_name
|
||||||
|
));
|
||||||
let repo_cfg = config
|
let repo_cfg = config
|
||||||
.binary_repos
|
.binary_repos
|
||||||
.get(repo_name)
|
.get(&item.repo_name)
|
||||||
.with_context(|| format!("Binary repo '{}' not found in config", repo_name))?;
|
.with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?;
|
||||||
db::repo::fetch_binary_package_archive(
|
let cached = db::repo::cache_binary_package_archive(
|
||||||
repo_name,
|
&item.repo_name,
|
||||||
repo_cfg,
|
repo_cfg,
|
||||||
rootfs,
|
&item.record,
|
||||||
record,
|
|
||||||
&config.package_cache_dir,
|
&config.package_cache_dir,
|
||||||
)?
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to cache binary package '{}' from repo '{}'",
|
||||||
|
item.record.filename, item.repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
binary_archives.insert(
|
||||||
|
(item.repo_name.clone(), item.record.filename.clone()),
|
||||||
|
cached,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
planner::PlanOrigin::Installed => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
ui::info(format!(
|
ui::info(format!(
|
||||||
"Executing planned step: {} ({})",
|
"Verifying checksums for {} binary package(s)...",
|
||||||
step.package,
|
binary_phase_items.len()
|
||||||
input_path.display()
|
));
|
||||||
|
for (idx, item) in binary_phase_items.iter().enumerate() {
|
||||||
|
ui::info(format!(
|
||||||
|
" [{}/{}] {}-{}",
|
||||||
|
idx + 1,
|
||||||
|
binary_phase_items.len(),
|
||||||
|
item.record.name,
|
||||||
|
item.record.version
|
||||||
|
));
|
||||||
|
let cached = binary_archives
|
||||||
|
.get(&(item.repo_name.clone(), item.record.filename.clone()))
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Cached archive missing for {} from repo '{}'",
|
||||||
|
item.record.filename, item.repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
db::repo::verify_binary_package_archive_checksums(&cached.package_path, &item.record)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Checksum verification failed for {} from repo '{}'",
|
||||||
|
item.record.filename, item.repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::info(format!(
|
||||||
|
"Verifying detached signatures for {} binary package(s)...",
|
||||||
|
binary_phase_items.len()
|
||||||
|
));
|
||||||
|
for (idx, item) in binary_phase_items.iter().enumerate() {
|
||||||
|
ui::info(format!(
|
||||||
|
" [{}/{}] {}-{}",
|
||||||
|
idx + 1,
|
||||||
|
binary_phase_items.len(),
|
||||||
|
item.record.name,
|
||||||
|
item.record.version
|
||||||
|
));
|
||||||
|
let repo_cfg = config
|
||||||
|
.binary_repos
|
||||||
|
.get(&item.repo_name)
|
||||||
|
.with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?;
|
||||||
|
let cached = binary_archives
|
||||||
|
.get(&(item.repo_name.clone(), item.record.filename.clone()))
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Cached archive missing for {} from repo '{}'",
|
||||||
|
item.record.filename, item.repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
db::repo::verify_binary_package_archive_signature(
|
||||||
|
&item.repo_name,
|
||||||
|
repo_cfg,
|
||||||
|
rootfs,
|
||||||
|
&cached.package_path,
|
||||||
|
&cached.signature_path,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Detached signature verification failed for {} from repo '{}'",
|
||||||
|
item.record.filename, item.repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
|
||||||
|
let total_steps = actionable_steps.len();
|
||||||
|
for (idx, step) in actionable_steps.into_iter().enumerate() {
|
||||||
|
match &step.origin {
|
||||||
|
planner::PlanOrigin::Source { path, .. } => {
|
||||||
|
ui::info(format!(
|
||||||
|
"[{}/{}] building+installing {} from source",
|
||||||
|
idx + 1,
|
||||||
|
total_steps,
|
||||||
|
step.package
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut cmd = std::process::Command::new(&exe);
|
let mut cmd = std::process::Command::new(&exe);
|
||||||
@@ -820,7 +1152,7 @@ fn execute_install_plan_with_child_commands(
|
|||||||
if clean {
|
if clean {
|
||||||
cmd.arg("--clean");
|
cmd.arg("--clean");
|
||||||
}
|
}
|
||||||
cmd.arg("install").arg(input_path);
|
cmd.arg("install").arg(path);
|
||||||
|
|
||||||
let status = cmd
|
let status = cmd
|
||||||
.status()
|
.status()
|
||||||
@@ -829,6 +1161,36 @@ fn execute_install_plan_with_child_commands(
|
|||||||
anyhow::bail!("Planned install step for '{}' failed", step.package);
|
anyhow::bail!("Planned install step for '{}' failed", step.package);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
planner::PlanOrigin::Binary { repo_name, record } => {
|
||||||
|
ui::info(format!(
|
||||||
|
"[{}/{}] installing {} {}-{} from binary:{}",
|
||||||
|
idx + 1,
|
||||||
|
total_steps,
|
||||||
|
record.name,
|
||||||
|
record.version,
|
||||||
|
record.revision,
|
||||||
|
repo_name
|
||||||
|
));
|
||||||
|
|
||||||
|
let cached = binary_archives
|
||||||
|
.get(&(repo_name.clone(), record.filename.clone()))
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Cached archive missing for planned binary step '{}' from repo '{}'",
|
||||||
|
record.filename, repo_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let staged = extract_package_archive_to_staging(&cached.package_path)?;
|
||||||
|
let spec = package_spec_from_repo_record(record);
|
||||||
|
let installed =
|
||||||
|
install_package_outputs_to_rootfs(&spec, staged.path(), rootfs, config)?;
|
||||||
|
for pkg in installed {
|
||||||
|
ui::success(format!("Installed {} v{}", pkg.name, pkg.version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
planner::PlanOrigin::Installed => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,94 +1327,7 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
if spec_path.to_string_lossy().ends_with(".tar.zst") {
|
if spec_path.to_string_lossy().ends_with(".tar.zst") {
|
||||||
// Install from archive
|
// Install from archive
|
||||||
ui::info(format!("Detected package archive: {}", spec_path.display()));
|
ui::info(format!("Detected package archive: {}", spec_path.display()));
|
||||||
let tmp_dir = tempfile::TempDir::new()?;
|
let (spec, tmp_dir) = load_package_archive_into_staging(&spec_path)?;
|
||||||
let extract_dir = tmp_dir.path().to_path_buf();
|
|
||||||
|
|
||||||
// Extract metadata.toml first to get spec
|
|
||||||
let file = fs::File::open(&spec_path)?;
|
|
||||||
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
|
|
||||||
let mut archive = tar::Archive::new(zstd_decoder);
|
|
||||||
|
|
||||||
let mut metadata_content = String::new();
|
|
||||||
for entry in archive.entries()? {
|
|
||||||
let mut entry = entry?;
|
|
||||||
if entry.path()?.to_string_lossy() == ".metadata.toml" {
|
|
||||||
use std::io::Read;
|
|
||||||
entry.read_to_string(&mut metadata_content)?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata_content.is_empty() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Package archive does not contain .metadata.toml: {}",
|
|
||||||
spec_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = fs::File::open(&spec_path)?;
|
|
||||||
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
|
|
||||||
let mut archive = tar::Archive::new(zstd_decoder);
|
|
||||||
archive.unpack(&extract_dir)?;
|
|
||||||
|
|
||||||
let metadata: toml::Value = toml::from_str(&metadata_content)?;
|
|
||||||
|
|
||||||
// Create a minimal spec from metadata
|
|
||||||
let mut spec = package::PackageSpec {
|
|
||||||
package: package::PackageInfo {
|
|
||||||
name: metadata
|
|
||||||
.get("name")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
version: metadata
|
|
||||||
.get("version")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
revision: metadata
|
|
||||||
.get("revision")
|
|
||||||
.and_then(|v| v.as_integer())
|
|
||||||
.unwrap_or(1) as u32,
|
|
||||||
description: metadata
|
|
||||||
.get("description")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
homepage: metadata
|
|
||||||
.get("homepage")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
license: parse_licenses_from_toml(&metadata),
|
|
||||||
},
|
|
||||||
packages: Vec::new(),
|
|
||||||
alternatives: package::Alternatives::default(),
|
|
||||||
manual_sources: Vec::new(),
|
|
||||||
source: Vec::new(),
|
|
||||||
build: package::Build {
|
|
||||||
build_type: package::BuildType::Bin,
|
|
||||||
flags: package::BuildFlags::default(),
|
|
||||||
},
|
|
||||||
dependencies: package::Dependencies {
|
|
||||||
build: Vec::new(),
|
|
||||||
runtime: parse_dependency_list(&metadata, "runtime"),
|
|
||||||
test: Vec::new(),
|
|
||||||
optional: parse_dependency_list(&metadata, "optional"),
|
|
||||||
},
|
|
||||||
package_alternatives: Default::default(),
|
|
||||||
package_dependencies: Default::default(),
|
|
||||||
spec_dir: PathBuf::from("."),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(provides) = metadata.get("provides").and_then(|v| v.as_array()) {
|
|
||||||
spec.alternatives.provides = provides
|
|
||||||
.iter()
|
|
||||||
.filter_map(|v| v.as_str())
|
|
||||||
.map(String::from)
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
(spec, Some(tmp_dir))
|
(spec, Some(tmp_dir))
|
||||||
} else {
|
} else {
|
||||||
// Install from spec (normal build)
|
// Install from spec (normal build)
|
||||||
@@ -1224,23 +1499,20 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !cli.lib32_only {
|
if !cli.lib32_only {
|
||||||
// 4. Stage (clean .la files, etc.)
|
if staging_dir.is_none() {
|
||||||
|
// Source-build path: apply staging transforms (strip/compress/static cleanup).
|
||||||
staging::process(&destdir, &pkg_spec)?;
|
staging::process(&destdir, &pkg_spec)?;
|
||||||
|
} else {
|
||||||
|
// Binary archive path: install as-packaged without post-build transformations.
|
||||||
|
ui::info("Installing binary archive payload without staging transforms");
|
||||||
|
}
|
||||||
|
|
||||||
// 5-6. Install/update to rootfs and register in DB
|
let installed =
|
||||||
for out in pkg_spec.outputs() {
|
install_package_outputs_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
|
||||||
let mut spec_for_out = pkg_spec.clone();
|
for pkg in installed {
|
||||||
let output_name = out.name.clone();
|
|
||||||
spec_for_out.package = out;
|
|
||||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
|
||||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
|
||||||
let out_destdir =
|
|
||||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
|
||||||
install_staged_to_rootfs(&spec_for_out, &out_destdir, &cli.rootfs, &config)?;
|
|
||||||
|
|
||||||
ui::success(format!(
|
ui::success(format!(
|
||||||
"Successfully installed {} v{}",
|
"Successfully installed {} v{}",
|
||||||
spec_for_out.package.name, spec_for_out.package.version
|
pkg.name, pkg.version
|
||||||
));
|
));
|
||||||
// TODO(snapper): create post-install snapshot after install commit succeeds.
|
// TODO(snapper): create post-install snapshot after install commit succeeds.
|
||||||
}
|
}
|
||||||
@@ -2106,4 +2378,67 @@ mod tests {
|
|||||||
assert!(!cfg.cache_dir.exists());
|
assert!(!cfg.cache_dir.exists());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> Result<()> {
|
||||||
|
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||||
|
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
|
||||||
|
let archive_path = pkg_dir.path().join("pkg-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||||
|
|
||||||
|
// Build an archive that intentionally does not contain .metadata.toml.
|
||||||
|
let file = fs::File::create(&archive_path)
|
||||||
|
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
|
||||||
|
let encoder =
|
||||||
|
zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?;
|
||||||
|
let mut tar = tar::Builder::new(encoder);
|
||||||
|
let payload = b"hello";
|
||||||
|
let mut header = tar::Header::new_gnu();
|
||||||
|
header.set_path("usr/bin/hello").unwrap();
|
||||||
|
header.set_size(payload.len() as u64);
|
||||||
|
header.set_mode(0o755);
|
||||||
|
header.set_cksum();
|
||||||
|
tar.append(&header, &payload[..]).unwrap();
|
||||||
|
let encoder = tar.into_inner().unwrap();
|
||||||
|
encoder.finish().unwrap();
|
||||||
|
|
||||||
|
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||||
|
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||||
|
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
||||||
|
|
||||||
|
let staged = extract_package_archive_to_staging(&archive_path)?;
|
||||||
|
let record = db::repo::BinaryRepoPackageRecord {
|
||||||
|
repo_name: "core".into(),
|
||||||
|
name: "pkg".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
filename: archive_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
size: payload.len() as u64,
|
||||||
|
sha256: String::new(),
|
||||||
|
sha512: String::new(),
|
||||||
|
description: Some("test package".into()),
|
||||||
|
homepage: Some("https://example.test".into()),
|
||||||
|
license: Some("MIT".into()),
|
||||||
|
provides: vec!["pkg-virtual".into()],
|
||||||
|
runtime_dependencies: vec!["glibc".into()],
|
||||||
|
optional_dependencies: vec!["manpages".into()],
|
||||||
|
};
|
||||||
|
let spec = package_spec_from_repo_record(&record);
|
||||||
|
let installed =
|
||||||
|
install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?;
|
||||||
|
|
||||||
|
assert_eq!(installed.len(), 1);
|
||||||
|
assert_eq!(installed[0].name, "pkg");
|
||||||
|
assert!(rootfs.path().join("usr/bin/hello").exists());
|
||||||
|
|
||||||
|
let db_path = cfg.db_dir.join("packages.db");
|
||||||
|
assert_eq!(
|
||||||
|
db::get_package_version(&db_path, "pkg")?,
|
||||||
|
Some("1.0".into())
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,11 +151,6 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
|||||||
|
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
|
||||||
crate::log_info!(
|
|
||||||
"Registered {} files and {} directories in database",
|
|
||||||
manifest.files.len(),
|
|
||||||
manifest.directories.len()
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+213
-93
@@ -112,8 +112,18 @@ pub struct BinaryRepoPackageRecord {
|
|||||||
pub sha256: String,
|
pub sha256: String,
|
||||||
pub sha512: String,
|
pub sha512: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub homepage: Option<String>,
|
||||||
|
pub license: Option<String>,
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
pub runtime_dependencies: Vec<String>,
|
pub runtime_dependencies: Vec<String>,
|
||||||
|
pub optional_dependencies: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local cache paths for a binary package archive and its detached signature.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BinaryRepoCachedArchive {
|
||||||
|
pub package_path: PathBuf,
|
||||||
|
pub signature_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File search hit returned from a cached binary repo database.
|
/// File search hit returned from a cached binary repo database.
|
||||||
@@ -1463,6 +1473,28 @@ fn query_package_runtime_deps(conn: &Connection, package_id: i64) -> Result<Vec<
|
|||||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_package_optional_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
||||||
|
let has_dependencies_table: bool = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='dependencies'",
|
||||||
|
[],
|
||||||
|
|r| {
|
||||||
|
let n: i64 = r.get(0)?;
|
||||||
|
Ok(n > 0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_dependencies_table {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT name FROM dependencies WHERE package_id = ?1 AND kind = 'optional' ORDER BY name",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
|
||||||
|
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
fn find_cached_binary_repo_packages(
|
fn find_cached_binary_repo_packages(
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
db_path: &Path,
|
db_path: &Path,
|
||||||
@@ -1481,7 +1513,9 @@ fn find_cached_binary_repo_packages(
|
|||||||
p.size,
|
p.size,
|
||||||
p.sha256,
|
p.sha256,
|
||||||
p.sha512,
|
p.sha512,
|
||||||
p.description
|
p.description,
|
||||||
|
p.homepage,
|
||||||
|
p.license
|
||||||
FROM packages p
|
FROM packages p
|
||||||
WHERE lower(p.name) = lower(?1)
|
WHERE lower(p.name) = lower(?1)
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
@@ -1508,8 +1542,11 @@ fn find_cached_binary_repo_packages(
|
|||||||
sha256: row.get(6)?,
|
sha256: row.get(6)?,
|
||||||
sha512: row.get(7)?,
|
sha512: row.get(7)?,
|
||||||
description: row.get(8)?,
|
description: row.get(8)?,
|
||||||
|
homepage: row.get(9)?,
|
||||||
|
license: row.get(10)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
@@ -1519,6 +1556,7 @@ fn find_cached_binary_repo_packages(
|
|||||||
let (package_id, mut rec) = row?;
|
let (package_id, mut rec) = row?;
|
||||||
rec.provides = query_package_provides(&conn, package_id)?;
|
rec.provides = query_package_provides(&conn, package_id)?;
|
||||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||||
|
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||||
out.push(rec);
|
out.push(rec);
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -1619,6 +1657,36 @@ fn verify_binary_package_record_checksums(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn download_binary_package_archive(
|
||||||
|
client: &reqwest::blocking::Client,
|
||||||
|
pkg_url: &str,
|
||||||
|
tmp_path: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
match copy_file_url_to_path(pkg_url, tmp_path)? {
|
||||||
|
FileUrlCopyOutcome::Copied => {}
|
||||||
|
FileUrlCopyOutcome::Missing => {
|
||||||
|
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
||||||
|
}
|
||||||
|
FileUrlCopyOutcome::NotFileUrl => {
|
||||||
|
let mut resp = client
|
||||||
|
.get(pkg_url)
|
||||||
|
.send()
|
||||||
|
.with_context(|| format!("Failed to fetch {}", pkg_url))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
anyhow::bail!("Failed to fetch {}: HTTP {}", pkg_url, resp.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = fs::File::create(tmp_path)
|
||||||
|
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
||||||
|
std::io::copy(&mut resp, &mut out)
|
||||||
|
.with_context(|| format!("Failed to save {}", tmp_path.display()))?;
|
||||||
|
out.flush()
|
||||||
|
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn fetch_binary_package_signature(
|
fn fetch_binary_package_signature(
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
repo: &crate::config::BinaryRepo,
|
repo: &crate::config::BinaryRepo,
|
||||||
@@ -1691,15 +1759,14 @@ fn verify_binary_package_signature(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a binary package archive and verify it against detached signatures
|
/// Ensure a binary package archive and detached signature are present in cache
|
||||||
/// and checksums from signed repository metadata.
|
/// without performing checksum/signature verification.
|
||||||
pub fn fetch_binary_package_archive(
|
pub fn cache_binary_package_archive(
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
repo: &crate::config::BinaryRepo,
|
repo: &crate::config::BinaryRepo,
|
||||||
rootfs: &Path,
|
|
||||||
rec: &BinaryRepoPackageRecord,
|
rec: &BinaryRepoPackageRecord,
|
||||||
package_cache_dir: &Path,
|
package_cache_dir: &Path,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<BinaryRepoCachedArchive> {
|
||||||
let machine_arch = std::env::consts::ARCH;
|
let machine_arch = std::env::consts::ARCH;
|
||||||
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
|
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -1711,8 +1778,8 @@ pub fn fetch_binary_package_archive(
|
|||||||
let cache_dir = binary_repo_packages_cache_dir(package_cache_dir, repo_name);
|
let cache_dir = binary_repo_packages_cache_dir(package_cache_dir, repo_name);
|
||||||
fs::create_dir_all(&cache_dir)
|
fs::create_dir_all(&cache_dir)
|
||||||
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
|
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
|
||||||
let dest_path = cache_dir.join(&rec.filename);
|
let package_path = cache_dir.join(&rec.filename);
|
||||||
let dest_sig_path = cache_dir.join(format!("{}.sig", rec.filename));
|
let signature_path = cache_dir.join(format!("{}.sig", rec.filename));
|
||||||
let tmp_path = cache_dir.join(format!("{}.tmp", rec.filename));
|
let tmp_path = cache_dir.join(format!("{}.tmp", rec.filename));
|
||||||
let tmp_sig_path = cache_dir.join(format!("{}.sig.tmp", rec.filename));
|
let tmp_sig_path = cache_dir.join(format!("{}.sig.tmp", rec.filename));
|
||||||
let pkg_url = join_repo_url(base_url, &rec.filename)?;
|
let pkg_url = join_repo_url(base_url, &rec.filename)?;
|
||||||
@@ -1722,105 +1789,94 @@ pub fn fetch_binary_package_archive(
|
|||||||
.build()
|
.build()
|
||||||
.context("Failed to build HTTP client for binary package fetch")?;
|
.context("Failed to build HTTP client for binary package fetch")?;
|
||||||
|
|
||||||
if dest_path.exists() {
|
let package_downloaded = if !package_path.exists() {
|
||||||
verify_binary_package_record_checksums(&dest_path, rec).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Cached binary package failed checksum verification: {}",
|
|
||||||
dest_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if !dest_sig_path.exists() {
|
|
||||||
let sig_downloaded =
|
|
||||||
fetch_binary_package_signature(repo_name, repo, &client, &sig_url, &tmp_sig_path)?;
|
|
||||||
if sig_downloaded {
|
|
||||||
fs::rename(&tmp_sig_path, &dest_sig_path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed to move {} to {}",
|
|
||||||
tmp_sig_path.display(),
|
|
||||||
dest_sig_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
} else {
|
|
||||||
let _ = fs::remove_file(&dest_sig_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verify_binary_package_signature(repo_name, repo, rootfs, &dest_path, &dest_sig_path)
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Cached binary package failed signature verification: {}",
|
|
||||||
dest_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
crate::log_info!("Using cached binary package: {}", dest_path.display());
|
|
||||||
return Ok(dest_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::log_info!("Fetching binary package: {}", pkg_url);
|
crate::log_info!("Fetching binary package: {}", pkg_url);
|
||||||
|
download_binary_package_archive(&client, &pkg_url, &tmp_path)?;
|
||||||
match copy_file_url_to_path(&pkg_url, &tmp_path)? {
|
fs::rename(&tmp_path, &package_path).with_context(|| {
|
||||||
FileUrlCopyOutcome::Copied => {}
|
|
||||||
FileUrlCopyOutcome::Missing => {
|
|
||||||
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
|
||||||
}
|
|
||||||
FileUrlCopyOutcome::NotFileUrl => {
|
|
||||||
let mut resp = client
|
|
||||||
.get(&pkg_url)
|
|
||||||
.send()
|
|
||||||
.with_context(|| format!("Failed to fetch {}", pkg_url))?;
|
|
||||||
if !resp.status().is_success() {
|
|
||||||
anyhow::bail!("Failed to fetch {}: HTTP {}", pkg_url, resp.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = fs::File::create(&tmp_path)
|
|
||||||
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
|
||||||
std::io::copy(&mut resp, &mut out)
|
|
||||||
.with_context(|| format!("Failed to save {}", tmp_path.display()))?;
|
|
||||||
out.flush()
|
|
||||||
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_binary_package_record_checksums(&tmp_path, rec).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Downloaded binary package failed checksum verification: {}",
|
|
||||||
rec.filename
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let sig_downloaded =
|
|
||||||
fetch_binary_package_signature(repo_name, repo, &client, &sig_url, &tmp_sig_path)?;
|
|
||||||
|
|
||||||
fs::rename(&tmp_path, &dest_path).with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"Failed to move {} to {}",
|
"Failed to move {} to {}",
|
||||||
tmp_path.display(),
|
tmp_path.display(),
|
||||||
dest_path.display()
|
package_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
crate::log_info!("Using cached binary package: {}", package_path.display());
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if package_downloaded || !signature_path.exists() {
|
||||||
|
let sig_downloaded =
|
||||||
|
fetch_binary_package_signature(repo_name, repo, &client, &sig_url, &tmp_sig_path)?;
|
||||||
if sig_downloaded {
|
if sig_downloaded {
|
||||||
fs::rename(&tmp_sig_path, &dest_sig_path).with_context(|| {
|
fs::rename(&tmp_sig_path, &signature_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to move {} to {}",
|
"Failed to move {} to {}",
|
||||||
tmp_sig_path.display(),
|
tmp_sig_path.display(),
|
||||||
dest_sig_path.display()
|
signature_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if let Err(err) =
|
|
||||||
verify_binary_package_signature(repo_name, repo, rootfs, &dest_path, &dest_sig_path)
|
|
||||||
{
|
|
||||||
let _ = fs::remove_file(&dest_path);
|
|
||||||
let _ = fs::remove_file(&dest_sig_path);
|
|
||||||
return Err(err).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Downloaded binary package failed signature verification: {}",
|
|
||||||
rec.filename
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let _ = fs::remove_file(&dest_sig_path);
|
let _ = fs::remove_file(&signature_path);
|
||||||
}
|
}
|
||||||
Ok(dest_path)
|
}
|
||||||
|
|
||||||
|
Ok(BinaryRepoCachedArchive {
|
||||||
|
package_path,
|
||||||
|
signature_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a cached/downloaded package archive against checksums from signed
|
||||||
|
/// repository metadata.
|
||||||
|
pub fn verify_binary_package_archive_checksums(
|
||||||
|
archive_path: &Path,
|
||||||
|
rec: &BinaryRepoPackageRecord,
|
||||||
|
) -> Result<()> {
|
||||||
|
verify_binary_package_record_checksums(archive_path, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a cached/downloaded package archive against its detached signature.
|
||||||
|
pub fn verify_binary_package_archive_signature(
|
||||||
|
repo_name: &str,
|
||||||
|
repo: &crate::config::BinaryRepo,
|
||||||
|
rootfs: &Path,
|
||||||
|
package_path: &Path,
|
||||||
|
signature_path: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
verify_binary_package_signature(repo_name, repo, rootfs, package_path, signature_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download a binary package archive and verify it against detached signatures
|
||||||
|
/// and checksums from signed repository metadata.
|
||||||
|
pub fn fetch_binary_package_archive(
|
||||||
|
repo_name: &str,
|
||||||
|
repo: &crate::config::BinaryRepo,
|
||||||
|
rootfs: &Path,
|
||||||
|
rec: &BinaryRepoPackageRecord,
|
||||||
|
package_cache_dir: &Path,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let cached = cache_binary_package_archive(repo_name, repo, rec, package_cache_dir)?;
|
||||||
|
verify_binary_package_archive_checksums(&cached.package_path, rec).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Binary package failed checksum verification: {}",
|
||||||
|
cached.package_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
verify_binary_package_archive_signature(
|
||||||
|
repo_name,
|
||||||
|
repo,
|
||||||
|
rootfs,
|
||||||
|
&cached.package_path,
|
||||||
|
&cached.signature_path,
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Binary package failed signature verification: {}",
|
||||||
|
cached.package_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(cached.package_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronize git mirrors into /usr/src/depot/<reponame>
|
/// Synchronize git mirrors into /usr/src/depot/<reponame>
|
||||||
@@ -2233,8 +2289,11 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
sha256,
|
sha256,
|
||||||
sha512,
|
sha512,
|
||||||
description: None,
|
description: None,
|
||||||
|
homepage: None,
|
||||||
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
|
optional_dependencies: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
|
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
|
||||||
@@ -2264,8 +2323,11 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
sha256,
|
sha256,
|
||||||
sha512,
|
sha512,
|
||||||
description: None,
|
description: None,
|
||||||
|
homepage: None,
|
||||||
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
|
optional_dependencies: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2343,6 +2405,64 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
assert!(PathBuf::from(format!("{}.sig", fetched.display())).exists());
|
assert!(PathBuf::from(format!("{}.sig", fetched.display())).exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_binary_package_archive_supports_phased_verification() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let repo_dir = tempfile::tempdir().unwrap();
|
||||||
|
let cache_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path());
|
||||||
|
std::fs::create_dir_all(&trusted_dir).unwrap();
|
||||||
|
|
||||||
|
let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
trusted_dir.join("repo.pub"),
|
||||||
|
keypair.pk.to_box().unwrap().to_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
|
||||||
|
let payload = b"staged verification payload";
|
||||||
|
let package_path = repo_dir.path().join(filename);
|
||||||
|
std::fs::write(&package_path, payload).unwrap();
|
||||||
|
|
||||||
|
let sig = minisign::sign(
|
||||||
|
Some(&keypair.pk),
|
||||||
|
&keypair.sk,
|
||||||
|
std::fs::File::open(&package_path).unwrap(),
|
||||||
|
None,
|
||||||
|
Some("test signature"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap();
|
||||||
|
|
||||||
|
let rec = test_record_for_payload(filename, payload);
|
||||||
|
let repo_url = url::Url::from_directory_path(repo_dir.path())
|
||||||
|
.expect("file URL")
|
||||||
|
.to_string();
|
||||||
|
let repo_cfg = crate::config::BinaryRepo {
|
||||||
|
url: repo_url,
|
||||||
|
allow_unsigned: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let cached = cache_binary_package_archive("repo", &repo_cfg, &rec, cache_dir.path())
|
||||||
|
.expect("cache should succeed");
|
||||||
|
assert!(cached.package_path.exists());
|
||||||
|
assert!(cached.signature_path.exists());
|
||||||
|
|
||||||
|
verify_binary_package_archive_checksums(&cached.package_path, &rec)
|
||||||
|
.expect("checksum verification should succeed");
|
||||||
|
verify_binary_package_archive_signature(
|
||||||
|
"repo",
|
||||||
|
&repo_cfg,
|
||||||
|
rootfs.path(),
|
||||||
|
&cached.package_path,
|
||||||
|
&cached.signature_path,
|
||||||
|
)
|
||||||
|
.expect("signature verification should succeed");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fetch_binary_package_archive_allows_missing_signature_when_configured() {
|
fn test_fetch_binary_package_archive_allows_missing_signature_when_configured() {
|
||||||
let rootfs = tempfile::tempdir().unwrap();
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -263,11 +263,11 @@ fn run_script_with_rootfs_context(
|
|||||||
.arg(rootfs)
|
.arg(rootfs)
|
||||||
.arg("/bin/sh")
|
.arg("/bin/sh")
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg("cd / && exec /bin/sh \"$1\"")
|
.arg("export DEPOT_PACKAGE DEPOT_ROOTFS DEPOT_ACTION DEPOT_PHASE; cd / && exec /bin/sh \"$1\"")
|
||||||
.arg("sh")
|
.arg("sh")
|
||||||
.arg(rel_script)
|
.arg(rel_script)
|
||||||
.env("DEPOT_PACKAGE", pkg_name)
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
.env("DEPOT_ROOTFS", rootfs)
|
.env("DEPOT_ROOTFS", "/")
|
||||||
.env("DEPOT_ACTION", hook.action())
|
.env("DEPOT_ACTION", hook.action())
|
||||||
.env("DEPOT_PHASE", hook.phase())
|
.env("DEPOT_PHASE", hook.phase())
|
||||||
.status()
|
.status()
|
||||||
|
|||||||
Reference in New Issue
Block a user