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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user