/* * download.c - source file downloader * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "mkpkg.h" #include #include #include #include #include #include #include #include #include static int is_url(const char *s) { return str_starts_with(s, "http://") || str_starts_with(s, "https://") || str_starts_with(s, "ftp://"); } static const char *url_basename(const char *url) { const char *slash = strrchr(url, '/'); return (slash && slash[1]) ? slash + 1 : "download"; } static void format_bytes(char *buf, size_t bufsz, long long bytes) { if (bytes >= (long long)1024 * 1024 * 1024) snprintf(buf, bufsz, "%.2f GB", (double)bytes / (1024.0 * 1024.0 * 1024.0)); else if (bytes >= 1024 * 1024) snprintf(buf, bufsz, "%.2f MB", (double)bytes / (1024.0 * 1024.0)); else if (bytes >= 1024) snprintf(buf, bufsz, "%.1f KB", (double)bytes / 1024.0); else snprintf(buf, bufsz, "%lld B", bytes); } /* ── per-download job ────────────────────────────────────────────────── */ typedef struct { pid_t pid; char dest[PATH_MAX_LEN]; char url[PATH_MAX_LEN]; int done; int status; } job_t; /* * Spawn wget for a single download. * stdout and stderr are silenced — mkpkg owns all terminal output. */ static pid_t spawn_wget(const char *url, const char *dest) { pid_t pid = fork(); if (pid < 0) err(E_DOWNLOAD, "fork"); if (pid == 0) { int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDOUT_FILENO); dup2(devnull, STDERR_FILENO); close(devnull); } execlp("wget", "wget", "--quiet", "-O", dest, url, (char *)NULL); _exit(E_DOWNLOAD); } return pid; } /* * Poll all jobs with WNOHANG, print a spinner + total bytes progress. * Returns number of failed downloads. */ static int wait_with_progress(job_t *jobs, int njobs) { static const char *spin[] = { "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏" }; int remaining = njobs; int failed = 0; int spin_idx = 0; while (remaining > 0) { int i; for (i = 0; i < njobs; i++) { int st; if (jobs[i].done) continue; if (waitpid(jobs[i].pid, &st, WNOHANG) == jobs[i].pid) { jobs[i].done = 1; jobs[i].status = st; remaining--; if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { failed++; unlink(jobs[i].dest); } } } { long long total = 0; int active = 0; struct stat st; for (i = 0; i < njobs; i++) { if (stat(jobs[i].dest, &st) == 0) { total += st.st_size; if (!jobs[i].done) active++; } } char bytes_str[32]; format_bytes(bytes_str, sizeof(bytes_str), total); fprintf(stderr, "\r %s %s (%d/%d active) ", spin[spin_idx % 10], bytes_str, active, njobs); fflush(stderr); spin_idx++; } if (remaining > 0) { struct timespec ts = { 0, 250000000L }; /* 250 ms */ nanosleep(&ts, NULL); } } fprintf(stderr, "\r%60s\r", ""); fflush(stderr); return failed; } /* ── download_sources ────────────────────────────────────────────────── */ int download_sources(pkgfile_t *pkgfile, config_t *config) { /* heap-allocated job array — avoids ~2 MB stack frame */ job_t *jobs = xmalloc(MAX_SOURCES * sizeof(job_t)); int njobs = 0; int i, failed; for (i = 0; i < pkgfile->source_count; i++) { source_t *src = &pkgfile->sources[i]; const char *url = src->url; char dest[PATH_MAX_LEN]; if (src->rename && strcmp(src->rename, "SKIP") == 0) continue; if (!is_url(url)) { /* local file — verify it exists */ char local[PATH_MAX_LEN]; snprintf(local, sizeof(local), "%s/%s", config->root_dir, url); if (access(local, F_OK) != 0) errx(E_DOWNLOAD, "source not found: %s", url); continue; } /* determine destination filename */ if (src->rename && src->rename[0]) { snprintf(dest, sizeof(dest), "%s/%s", config->source_dir, src->rename); } else { snprintf(dest, sizeof(dest), "%s/%s", config->source_dir, url_basename(url)); } if (access(dest, F_OK) == 0) { info("already exists: %s", dest); continue; } info("downloading %s", url); jobs[njobs].done = 0; jobs[njobs].status = 0; jobs[njobs].pid = spawn_wget(url, dest); snprintf(jobs[njobs].dest, sizeof(jobs[njobs].dest), "%s", dest); snprintf(jobs[njobs].url, sizeof(jobs[njobs].url), "%s", url); njobs++; } if (njobs == 0) { free(jobs); return 0; } failed = wait_with_progress(jobs, njobs); for (i = 0; i < njobs; i++) { if (jobs[i].done && (!WIFEXITED(jobs[i].status) || WEXITSTATUS(jobs[i].status) != 0)) warning("download failed: %s", jobs[i].url); } free(jobs); if (failed) errx(E_DOWNLOAD, "%d download(s) failed", failed); info("downloaded %d file%s", njobs, njobs == 1 ? "" : "s"); return 0; }