Add git sources, check-updates, repos submodule, improve docs

dpack features:
- Git source support: packages can specify [source].git for cloning
  instead of tarball download. Supports branch, tag, and commit pinning.
  SHA256 can be set to "SKIP" for git sources.
- check-updates command: queries upstream APIs (GitHub releases/tags)
  to find available updates. Packages set [source].update_check URL.
- CheckUpdates CLI subcommand wired into main.rs

Package changes:
- FreeCAD updated to weekly-2026.03.19 development builds
- dwl: added update_check URL and git source documentation
- src/repos extracted to standalone git repo (danny8632/repos.git)
  and added as git submodule

Documentation:
- All 7 README.md files updated with detailed requirements sections
  including which Linux distros are supported, exact package names
  for Arch/Ubuntu/Fedora, and clear notes about which components
  require Linux vs can be built on macOS
- dpack README: added git source and check-updates documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 11:58:23 +01:00
parent 029642ae5b
commit 5fb597e2d6
136 changed files with 509 additions and 2731 deletions

View File

@@ -16,12 +16,47 @@ A source-based package manager for DarkForge Linux, positioned between CRUX's `p
## Requirements
- Rust 1.75+ (build)
- Linux (runtime — uses Linux namespaces for sandboxing)
- bubblewrap (`bwrap`) for sandboxed builds (optional, falls back to direct execution)
- `curl` or `wget` for source downloads
- `tar` for source extraction
- `readelf` or `objdump` for shared library scanning
**Build-time (compiling dpack itself):**
dpack is written in Rust and can be built on any platform with a Rust toolchain:
- Rust 1.75+ with Cargo (install via https://rustup.rs)
- A C linker (gcc or clang) — needed by some Rust dependencies
- Works on Linux and macOS for development, but runtime features require Linux
```bash
# macOS (Homebrew)
brew install rustup && rustup-init
# Arch Linux
sudo pacman -S rust
# Ubuntu / Debian
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
**Runtime (using dpack to build/install packages):**
dpack must run on a Linux system (it uses Linux-specific features):
- Linux kernel 5.4+ (for namespace support)
- `bash` (build scripts are bash)
- `curl` or `wget` (source tarball downloads)
- `git` (for git-source packages)
- `tar` (source extraction)
- `readelf` or `objdump` (shared library scanning — part of binutils)
- bubblewrap (`bwrap`) for sandboxed builds (optional — falls back to direct execution)
- A C/C++ compiler (gcc or clang) and make (for building packages)
On the DarkForge system itself, all runtime dependencies are provided by the base system. On another Linux distro for testing:
```bash
# Arch Linux
sudo pacman -S base-devel bubblewrap curl git
# Ubuntu / Debian
sudo apt install build-essential bubblewrap curl git binutils
```
## Building
@@ -33,7 +68,11 @@ cargo build --release
The binary is at `target/release/dpack`. Install it:
```bash
# On Linux
sudo install -m755 target/release/dpack /usr/local/bin/
# For development/testing (from the repo root)
cargo run --release -- install zlib
```
## Usage
@@ -64,6 +103,9 @@ dpack list
# Check for file conflicts and shared library issues
dpack check
# Check for available updates (compares installed vs repo + upstream)
dpack check-updates
# Convert foreign package formats
dpack convert /path/to/Pkgfile # CRUX → dpack TOML (stdout)
dpack convert /path/to/curl-8.19.0.ebuild -o curl.toml # Gentoo → dpack TOML (file)
@@ -158,6 +200,42 @@ ldflags = ""
The `system` field is a hint: `autotools`, `cmake`, `meson`, `cargo`, or `custom`.
### Git sources
Instead of downloading a tarball, dpack can clone a git repository directly. This is useful for building from the latest development branch or a specific commit:
```toml
[source]
url = "" # can be empty for git sources
sha256 = "SKIP" # integrity verified by git
git = "https://github.com/FreeCAD/FreeCAD.git" # clone URL
branch = "main" # checkout this branch
# Or pin to a tag (supports ${version} expansion):
# tag = "v${version}"
# Or pin to a specific commit:
# commit = "abc123def456"
```
When `git` is set, dpack clones the repository (with `--depth 1` for branches/tags) into the build directory. The `branch`, `tag`, and `commit` fields control what gets checked out (in priority order: commit > tag > branch > default).
### Upstream update checking
Packages can specify an `update_check` URL in the `[source]` section. When you run `dpack check-updates`, it queries these URLs and compares the result against your installed version.
```toml
[source]
url = "https://example.com/foo-${version}.tar.xz"
sha256 = "..."
update_check = "https://api.github.com/repos/owner/repo/releases/latest"
```
Supported URL patterns:
- **GitHub releases API** — parses `tag_name` from the JSON response
- **GitHub/Gitea tags API** — parses the first tag name
- **Plain URL** — the response body is treated as the version string
## Running Tests
```bash

View File

@@ -94,13 +94,6 @@ impl BuildOrchestrator {
let ident = pkg.ident();
println!(">>> Building {}", ident);
// Step 1: Download source
let source_path = self.download_source(pkg)?;
// Step 2: Verify checksum
self.verify_checksum(&source_path, &pkg.source.sha256)?;
// Step 3: Extract source
let build_dir = self.config.paths.build_dir.join(&ident);
let staging_dir = self.config.paths.build_dir.join(format!("{}-staging", ident));
@@ -108,11 +101,17 @@ impl BuildOrchestrator {
let _ = std::fs::remove_dir_all(&build_dir);
let _ = std::fs::remove_dir_all(&staging_dir);
self.extract_source(&source_path, &build_dir)?;
// Step 4: Apply patches
// Find the actual source directory (tarballs often have a top-level dir)
let actual_build_dir = find_source_dir(&build_dir)?;
let actual_build_dir = if pkg.source.is_git() {
// Git source: clone the repo
self.clone_git_source(pkg, &build_dir)?;
build_dir.clone()
} else {
// Tarball source: download, verify, extract
let source_path = self.download_source(pkg)?;
self.verify_checksum(&source_path, &pkg.source.sha256)?;
self.extract_source(&source_path, &build_dir)?;
find_source_dir(&build_dir)?
};
// Step 5: Build in sandbox
let sandbox = BuildSandbox::new(
@@ -162,6 +161,66 @@ impl BuildOrchestrator {
Ok(())
}
/// Clone a git repository source into the build directory.
fn clone_git_source(&self, pkg: &PackageDefinition, build_dir: &Path) -> Result<()> {
use crate::config::package::GitRef;
let git_url = &pkg.source.git;
log::info!("Cloning git source: {}", git_url);
let mut cmd = std::process::Command::new("git");
cmd.arg("clone");
// Depth 1 for tags/branches (faster), full clone for commits
match pkg.source.git_ref() {
GitRef::Commit(_) => {} // need full history for arbitrary commits
_ => { cmd.arg("--depth").arg("1"); }
}
// Branch selection
match pkg.source.git_ref() {
GitRef::Branch(ref branch) => {
cmd.arg("--branch").arg(branch);
}
GitRef::Tag(ref tag) => {
let expanded = tag.replace("${version}", &pkg.package.version);
cmd.arg("--branch").arg(&expanded);
}
_ => {}
}
cmd.arg(git_url).arg(build_dir);
let status = cmd.status().context("Failed to run git clone")?;
if !status.success() {
bail!("Git clone failed for: {}", git_url);
}
// If a specific commit was requested, checkout that commit
if let GitRef::Commit(ref hash) = pkg.source.git_ref() {
let status = std::process::Command::new("git")
.args(["checkout", hash])
.current_dir(build_dir)
.status()
.context("Failed to checkout commit")?;
if !status.success() {
bail!("Git checkout failed for commit: {}", hash);
}
}
// Log what we got
let head = std::process::Command::new("git")
.args(["log", "--oneline", "-1"])
.current_dir(build_dir)
.output();
if let Ok(output) = head {
let rev = String::from_utf8_lossy(&output.stdout);
log::info!("Cloned at: {}", rev.trim());
}
Ok(())
}
/// Download the source tarball to the source cache.
fn download_source(&self, pkg: &PackageDefinition) -> Result<PathBuf> {
let url = pkg.expanded_source_url();
@@ -337,3 +396,129 @@ fn calculate_dir_size(dir: &Path) -> u64 {
.map(|e| e.metadata().map_or(0, |m| m.len()))
.sum()
}
/// Result of an update check for a single package.
#[derive(Debug)]
pub struct UpdateCheckResult {
pub name: String,
pub installed_version: String,
pub repo_version: String,
pub upstream_version: Option<String>,
pub has_repo_update: bool,
pub has_upstream_update: bool,
}
/// Check for available updates across all installed packages.
///
/// Compares installed versions against:
/// 1. The repo definition version (always checked)
/// 2. The upstream latest release via `update_check` URL (if configured)
pub fn check_updates(
db: &PackageDb,
all_packages: &std::collections::HashMap<String, PackageDefinition>,
) -> Vec<UpdateCheckResult> {
let mut results = Vec::new();
for installed in db.list_all() {
let repo_version = all_packages
.get(&installed.name)
.map(|p| p.package.version.clone());
let has_repo_update = repo_version
.as_ref()
.map_or(false, |rv| rv != &installed.version);
// Check upstream if update_check URL is configured
let upstream_version = all_packages
.get(&installed.name)
.and_then(|p| {
if p.source.update_check.is_empty() {
return None;
}
fetch_upstream_version(&p.source.update_check).ok()
});
let has_upstream_update = upstream_version
.as_ref()
.map_or(false, |uv| {
let rv = repo_version.as_deref().unwrap_or(&installed.version);
uv != rv
});
if has_repo_update || has_upstream_update {
results.push(UpdateCheckResult {
name: installed.name.clone(),
installed_version: installed.version.clone(),
repo_version: repo_version.unwrap_or_else(|| "?".to_string()),
upstream_version,
has_repo_update,
has_upstream_update,
});
}
}
results
}
/// Fetch the latest version from an upstream update check URL.
///
/// Supports:
/// - GitHub releases API: `https://api.github.com/repos/<owner>/<repo>/releases/latest`
/// Parses the `tag_name` field from the JSON response.
/// - GitHub tags: `https://api.github.com/repos/<owner>/<repo>/tags`
/// Returns the first tag name.
/// - Plain URL: returns the trimmed response body as the version string.
fn fetch_upstream_version(url: &str) -> Result<String> {
let output = std::process::Command::new("curl")
.args(["-sfL", "--max-time", "10", url])
.output()
.context("Failed to run curl for update check")?;
if !output.status.success() {
bail!("Update check failed for: {}", url);
}
let body = String::from_utf8_lossy(&output.stdout);
// GitHub releases API: parse tag_name from JSON
if url.contains("api.github.com") && url.contains("/releases/") {
// Simple JSON extraction without a JSON parser dep
// Look for "tag_name": "v1.2.3"
if let Some(pos) = body.find("\"tag_name\"") {
let rest = &body[pos..];
if let Some(start) = rest.find(':') {
let value_part = rest[start + 1..].trim();
if value_part.starts_with('"') {
if let Some(end) = value_part[1..].find('"') {
let tag = &value_part[1..end + 1];
// Strip leading 'v' if present
let version = tag.strip_prefix('v').unwrap_or(tag);
return Ok(version.to_string());
}
}
}
}
bail!("Could not parse tag_name from GitHub API response");
}
// GitHub tags API: parse first tag name
if url.contains("api.github.com") && url.contains("/tags") {
if let Some(pos) = body.find("\"name\"") {
let rest = &body[pos..];
if let Some(start) = rest.find(':') {
let value_part = rest[start + 1..].trim();
if value_part.starts_with('"') {
if let Some(end) = value_part[1..].find('"') {
let tag = &value_part[1..end + 1];
let version = tag.strip_prefix('v').unwrap_or(tag);
return Ok(version.to_string());
}
}
}
}
bail!("Could not parse name from GitHub tags response");
}
// Plain URL: return trimmed body
Ok(body.trim().to_string())
}

View File

@@ -55,14 +55,74 @@ pub struct SourceInfo {
/// Download URL. May contain `${version}` which is expanded at runtime.
pub url: String,
/// SHA256 checksum of the source tarball
/// SHA256 checksum of the source tarball.
/// Set to "SKIP" for git sources (integrity is verified by the VCS itself).
pub sha256: String,
/// Optional: git repository URL (used instead of tarball when set).
/// When this is set, `url` becomes the upstream project URL for reference,
/// and `git` is the actual clone URL.
#[serde(default)]
pub git: String,
/// Git branch to checkout (default: repo's default branch, usually main/master).
#[serde(default)]
pub branch: String,
/// Git tag to checkout. Takes precedence over branch.
/// May contain `${version}` (e.g., `v${version}`).
#[serde(default)]
pub tag: String,
/// Git commit hash to pin to (overrides branch and tag).
#[serde(default)]
pub commit: String,
/// URL pattern for checking upstream releases (for `dpack check-updates`).
/// Supports: GitHub releases API, GitLab tags API, or a plain URL returning
/// the latest version string.
/// Example: "https://api.github.com/repos/FreeCAD/FreeCAD/releases/latest"
#[serde(default)]
pub update_check: String,
/// Optional: additional source files or patches to download
#[serde(default)]
pub patches: Vec<PatchInfo>,
}
impl SourceInfo {
/// Returns true if this source should be fetched via git clone.
pub fn is_git(&self) -> bool {
!self.git.is_empty()
}
/// Returns the effective git ref to checkout.
pub fn git_ref(&self) -> GitRef {
if !self.commit.is_empty() {
GitRef::Commit(self.commit.clone())
} else if !self.tag.is_empty() {
GitRef::Tag(self.tag.clone())
} else if !self.branch.is_empty() {
GitRef::Branch(self.branch.clone())
} else {
GitRef::Default
}
}
}
/// Which git ref to checkout after cloning.
#[derive(Debug, Clone)]
pub enum GitRef {
/// Use the repo's default branch (main/master)
Default,
/// Checkout a specific branch
Branch(String),
/// Checkout a specific tag
Tag(String),
/// Checkout a specific commit hash
Commit(String),
}
/// A patch to apply to the source before building.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchInfo {
@@ -210,12 +270,21 @@ impl PackageDefinition {
fn validate(&self) -> Result<()> {
anyhow::ensure!(!self.package.name.is_empty(), "Package name cannot be empty");
anyhow::ensure!(!self.package.version.is_empty(), "Package version cannot be empty");
anyhow::ensure!(!self.source.url.is_empty(), "Source URL cannot be empty");
anyhow::ensure!(
self.source.sha256.len() == 64 && self.source.sha256.chars().all(|c| c.is_ascii_hexdigit()),
"SHA256 checksum must be exactly 64 hex characters, got: '{}'",
self.source.sha256
);
// For git sources, the URL can be empty (git field is used instead)
if !self.source.is_git() {
anyhow::ensure!(!self.source.url.is_empty(), "Source URL cannot be empty (set [source].git for git sources)");
}
// SHA256 can be "SKIP" for git sources, otherwise must be 64 hex chars
if self.source.sha256 != "SKIP" {
anyhow::ensure!(
self.source.sha256.len() == 64 && self.source.sha256.chars().all(|c| c.is_ascii_hexdigit()),
"SHA256 checksum must be exactly 64 hex characters or 'SKIP' for git sources, got: '{}'",
self.source.sha256
);
}
anyhow::ensure!(!self.build.install.is_empty(), "Install command cannot be empty");
// Validate optional dep names don't contain spaces or special chars

View File

@@ -86,6 +86,9 @@ enum Commands {
/// Check for shared library conflicts
Check,
/// Check for available package updates (repo + upstream)
CheckUpdates,
}
fn main() {
@@ -383,6 +386,50 @@ fn run(cli: Cli) -> Result<()> {
}
}
}
Commands::CheckUpdates => {
let db = PackageDb::open(&config.paths.db_dir)?;
// Load all repos
let mut all_repo_packages = std::collections::HashMap::new();
for repo in &config.repos {
let repo_pkgs = resolver::DependencyGraph::load_repo(&repo.path)?;
all_repo_packages.extend(repo_pkgs);
}
println!("Checking for updates...\n");
let results = build::check_updates(&db, &all_repo_packages);
if results.is_empty() {
println!("{}", "All packages are up to date.".green());
} else {
println!(
"{} update(s) available:\n",
results.len().to_string().yellow().bold()
);
for r in &results {
let mut line = format!(
" {} {}{}",
r.name.bold(),
r.installed_version.red(),
r.repo_version.green()
);
if let Some(ref uv) = r.upstream_version {
if r.has_upstream_update {
line.push_str(&format!(
" (upstream: {})",
uv.cyan()
));
}
}
println!("{}", line);
}
println!(
"\nRun {} to apply updates.",
"dpack upgrade".bold()
);
}
}
}
Ok(())