feat: add repo planning/binary repo support and Meson build integration

- add repos.toml source/binary repo configuration with arch overrides and priorities
- add dependency planner/execution plan flow with interactive provider selection and --yes support
- expand binary repo DB metadata (sha512, dependencies, file lists) and repo owns/search helpers
- add rootfs-scoped fd locking and minisign detached signing/verification helpers
- add Meson build/test support for the project with builddir-local CARGO_HOME/CARGO_TARGET_DIR
- install contrib example configs into sysconfdir (/etc/depot.d layout) via Meson
- improve builder env handling (sanitized PATH, tool env passthrough, CXXFLAGS support)
- support += appends in package specs and add build.flags.cxxflags parsing
- prompt for git credentials when private repo auth is required
- update docs/examples and dependency setup (system libs, zstdmt, tempfile pin)
This commit is contained in:
2026-02-24 21:38:12 -06:00
parent d63ad03e98
commit 9953d4b2ee
30 changed files with 4536 additions and 111 deletions
+139 -8
View File
@@ -1,9 +1,11 @@
//! Git source support via libgit2 (git2 crate)
use anyhow::{Context, Result};
use git2::{Cred, FetchOptions, Oid, RemoteCallbacks, Repository};
use git2::{Cred, CredentialType, FetchOptions, Oid, RemoteCallbacks, Repository};
use inquire::Password;
use sha2::{Digest, Sha256};
use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::Path;
/// Checkout a repository URL at a specific revision into `checkout_dir`.
@@ -115,15 +117,144 @@ fn checkout_rev(repo: &Repository, rev: &str) -> Result<()> {
fn remote_callbacks() -> RemoteCallbacks<'static> {
let mut callbacks = RemoteCallbacks::new();
let mut credential_state = CredentialState::default();
// Try SSH agent / default key locations.
callbacks.credentials(|_url, username_from_url, _allowed| {
if let Some(name) = username_from_url {
Cred::ssh_key_from_agent(name)
} else {
Cred::default()
}
callbacks.credentials(move |url, username_from_url, allowed| {
credential_state.provide(url, username_from_url, allowed)
});
callbacks
}
#[derive(Default)]
struct CredentialState {
username: Option<String>,
prompted_userpass: bool,
}
impl CredentialState {
fn provide(
&mut self,
url: &str,
username_from_url: Option<&str>,
allowed: CredentialType,
) -> std::result::Result<Cred, git2::Error> {
if allowed.contains(CredentialType::USERNAME)
&& !allowed.intersects(
CredentialType::SSH_KEY
| CredentialType::USER_PASS_PLAINTEXT
| CredentialType::DEFAULT,
)
{
let username = self.username(url, username_from_url)?;
return Cred::username(&username);
}
if allowed.contains(CredentialType::SSH_KEY) {
let ssh_username = username_from_url
.or(self.username.as_deref())
.unwrap_or("git");
if let Ok(cred) = Cred::ssh_key_from_agent(ssh_username) {
return Ok(cred);
}
}
if allowed.contains(CredentialType::DEFAULT)
&& let Ok(cred) = Cred::default()
{
return Ok(cred);
}
if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) {
let (username, password) = self.userpass(url, username_from_url)?;
return Cred::userpass_plaintext(&username, &password);
}
if allowed.contains(CredentialType::USERNAME) {
let username = self.username(url, username_from_url)?;
return Cred::username(&username);
}
Err(git2::Error::from_str(
"Unsupported authentication method requested by git remote",
))
}
fn username(
&mut self,
url: &str,
username_from_url: Option<&str>,
) -> std::result::Result<String, git2::Error> {
if let Some(username) = username_from_url
&& !username.trim().is_empty()
{
let username = username.trim().to_string();
self.username.get_or_insert_with(|| username.clone());
return Ok(username);
}
if let Some(username) = self.username.as_ref() {
return Ok(username.clone());
}
ensure_prompt_terminal(url)?;
crate::log_warn!("Git remote requires credentials: {}", url);
let mut input = String::new();
loop {
print!("Git username for {}: ", url);
io::stdout()
.flush()
.map_err(|e| git2::Error::from_str(&format!("Failed to flush prompt: {e}")))?;
input.clear();
io::stdin()
.read_line(&mut input)
.map_err(|e| git2::Error::from_str(&format!("Failed to read username: {e}")))?;
let trimmed = input.trim();
if !trimmed.is_empty() {
let username = trimmed.to_string();
self.username = Some(username.clone());
return Ok(username);
}
crate::log_warn!("Username cannot be empty.");
}
}
fn userpass(
&mut self,
url: &str,
username_from_url: Option<&str>,
) -> std::result::Result<(String, String), git2::Error> {
if self.prompted_userpass {
return Err(git2::Error::from_str(
"Git credentials were rejected by the remote",
));
}
let username = self.username(url, username_from_url)?;
ensure_prompt_terminal(url)?;
self.prompted_userpass = true;
let prompt = format!("Git password/token for {} ({}):", url, username);
let password = Password::new(&prompt)
.without_confirmation()
.prompt()
.map_err(|e| {
git2::Error::from_str(&format!("Failed to read git password/token: {e}"))
})?;
if password.is_empty() {
return Err(git2::Error::from_str("Git password/token cannot be empty"));
}
Ok((username, password))
}
}
fn ensure_prompt_terminal(url: &str) -> std::result::Result<(), git2::Error> {
if io::stdin().is_terminal() && io::stdout().is_terminal() {
return Ok(());
}
Err(git2::Error::from_str(&format!(
"Authentication required for {url}, but no interactive terminal is available"
)))
}