Makefile
Rama main · réplica de lectura del repositorio original.
# SPDX-License-Identifier: MIT
# HeapLeach — parallel bulk downloader
#
# The default build runs entirely inside Docker (no local Go or Node needed)
# and exports the application and its ocr/ data to ./bin on the host.
# The TypeScript UI is compiled and embedded into the binary.
SHELL := /bin/bash
.DEFAULT_GOAL := help
BIN_DIR := bin
DIST_DIR := dist
BINARY := $(BIN_DIR)/heapleach
# cgo needs a C toolchain for each target. Releases use native CI runners;
# local archives default to this host. Set RELEASE_TARGETS and CC to cross-build.
RELEASE_TARGETS ?= $(shell go env GOOS)/$(shell go env GOARCH)
IMAGE ?= heapleach
TAG ?= latest
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
PORT ?= 8080
DOWNLOADS ?= $(HOME)/Downloads
CONCURRENCY ?= 4
DIST := backend/internal/webui/dist
# What the exported binary is made of, so `make build` can tell a stale
# binary from a current one and do nothing when there is nothing to do.
#
# The frontend counts as much as the Go does: it is compiled into the binary,
# so a UI edit leaves it just as out of date as an extractor edit would. The
# embed directory itself is excluded — it is build output, rewritten by every
# frontend build, and treating it as a source would mean never settling.
GO_SOURCES := $(shell find backend -type f \( -name '*.go' -o -name '*.sh' \) -not -path '$(DIST)/*' 2>/dev/null) \
backend/go.mod backend/go.sum
UI_SOURCES := $(shell find frontend -type f \
\( -name '*.ts' -o -name '*.tsx' -o -name '*.css' -o -name '*.html' -o -name '*.json' \) \
-not -path 'frontend/node_modules/*' -not -path 'frontend/dist/*' 2>/dev/null)
BUILD_SOURCES := $(GO_SOURCES) $(UI_SOURCES) Dockerfile Makefile .dockerignore helpers/captcha/install.sh
# Optional helpers the service uses when present: yt-dlp resolves YouTube,
# ffmpeg rewraps and muxes, and deno runs the player JavaScript YouTube signs
# its media URLs with — yt-dlp has deprecated extracting without a runtime.
# Local OCR runs in-process using a pinned model and native runtime.
# `make dependencies` puts these helpers in ./bin, where the service looks
# before falling back to PATH.
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),aarch64)
YTDLP_ASSET := yt-dlp_linux_aarch64
FFMPEG_ASSET := ffmpeg-master-latest-linuxarm64-gpl.tar.xz
DENO_ASSET := deno-aarch64-unknown-linux-gnu.zip
else
YTDLP_ASSET := yt-dlp_linux
FFMPEG_ASSET := ffmpeg-master-latest-linux64-gpl.tar.xz
DENO_ASSET := deno-x86_64-unknown-linux-gnu.zip
endif
YTDLP_URL := https://github.com/yt-dlp/yt-dlp/releases/latest/download/$(YTDLP_ASSET)
FFMPEG_URL := https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/$(FFMPEG_ASSET)
DENO_URL := https://github.com/denoland/deno/releases/latest/download/$(DENO_ASSET)
NODE_IMAGE := node:24-alpine
UID_GID := $(shell id -u):$(shell id -g)
# Prefer the host toolchain when it exists, otherwise fall back to Docker.
HAVE_NPM := $(shell command -v npm 2>/dev/null)
HAVE_GO := $(shell command -v go 2>/dev/null)
.PHONY: help build binary run image run-image stop logs shell dev dev-backend dev-frontend \
frontend frontend-clean screenshots dist tag native test test-frontend test-live check fmt fmt-check vet tidy lock dependencies \
hosts hosts-check ocr-assets captcha-helper test-captcha test-release spdx-check \
clean distclean
## help: show this help
help:
@echo "HeapLeach — parallel bulk downloader"
@echo
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / make /' | column -t -s ':'
@echo
@echo " Variables: PORT=$(PORT) (run-image)"
@echo " DOWNLOADS=$(DOWNLOADS) IMAGE=$(IMAGE):$(TAG)"
## build: build in Docker and export the application and OCR data into ./bin
build: $(BINARY) $(BIN_DIR)/ocr/VERSIONS
# The rule that does the work. Its prerequisites are the sources the binary
# is made of, which is what lets `build` be asked for freely: it is a no-op
# while the binary is newer than all of them, and it rebuilds the moment one
# changes. Anything that runs the binary depends on this rather than on the
# file existing, since a stale binary is exactly what looks like a bug.
$(BINARY): $(BUILD_SOURCES) | $(BIN_DIR)
@echo ">> building in Docker, exporting binary to $(BINARY)"
DOCKER_BUILDKIT=1 docker build \
--target export \
--build-arg VERSION=$(VERSION) \
--output type=local,dest=$(BIN_DIR) \
-f Dockerfile .
@chmod +x $(BINARY)
@# BuildKit writes the file with the timestamp it had in the image, which
@# can predate the sources and would leave the target permanently stale.
@touch $(BINARY)
@echo ">> $(BINARY) ($$(du -h $(BINARY) | cut -f1)) — UI embedded, OCR data in $(BIN_DIR)/ocr"
## binary: alias for build
binary: build
## run: build if needed, serve on a free local port and open the browser
run: build
$(BINARY) -addr 127.0.0.1:0 -open "$(DOWNLOADS)"
## image: build the runnable Docker image
image:
DOCKER_BUILDKIT=1 docker build \
--target runtime \
--build-arg VERSION=$(VERSION) \
-t $(IMAGE):$(TAG) \
-f Dockerfile .
@echo ">> built $(IMAGE):$(TAG)"
## run-image: run the container image instead of the host binary
run-image: image
@mkdir -p "$(DOWNLOADS)"
docker run --rm -it \
--name heapleach \
-p $(PORT):8080 \
-e HEAPLEACH_CONCURRENCY=$(CONCURRENCY) \
-v "$(DOWNLOADS)":/downloads \
--user $(UID_GID) \
$(IMAGE):$(TAG)
## stop: stop a detached container started by `make run-image`
stop:
-docker stop heapleach
## logs: follow container logs
logs:
docker logs -f heapleach
## shell: open a shell in the runtime image
shell: image
docker run --rm -it --entrypoint sh $(IMAGE):$(TAG)
## native: build on the host (needs Go; uses Docker for the UI if Node is absent)
native: frontend ocr-assets $(BIN_DIR)
ifndef HAVE_GO
$(error Go is not installed — use `make build` to build in Docker instead)
endif
cd backend && CGO_ENABLED=1 go build -trimpath \
-ldflags "-s -w -X main.version=$(VERSION)" \
-o ../$(BINARY) ./cmd/heapleach
@echo ">> $(BINARY)"
## dist: build release archives for this host (or RELEASE_TARGETS with a suitable CC)
dist: frontend
ifndef HAVE_GO
$(error Go is not installed — a release build needs it on the host)
endif
@rm -rf $(DIST_DIR) && mkdir -p $(DIST_DIR)
@set -euo pipefail; \
for target in $(RELEASE_TARGETS); do \
goos=$${target%%/*}; goarch=$${target##*/}; \
binary=heapleach; \
[ "$$goos" = windows ] && binary=heapleach.exe; \
stage=$$(mktemp -d); \
( cd backend && CGO_ENABLED=1 GOOS=$$goos GOARCH=$$goarch go build -trimpath \
-ldflags "-s -w -X main.version=$(VERSION)" \
-o "$$stage/$$binary" ./cmd/heapleach ); \
cp README.md LICENSE "$$stage/"; \
bash helpers/captcha/install.sh "$$target" "$$stage/ocr"; \
base="heapleach_$(VERSION)_$${goos}_$${goarch}"; \
if [ "$$goos" = windows ]; then \
if command -v zip >/dev/null 2>&1; then \
( cd "$$stage" && zip -qr - . ) > "$(DIST_DIR)/$$base.zip"; \
else \
( cd "$$stage" && 7z a -tzip "$(abspath $(DIST_DIR))/$$base.zip" . >/dev/null ); \
fi; \
else \
tar -czf "$(DIST_DIR)/$$base.tar.gz" -C "$$stage" .; \
fi; \
rm -rf "$$stage"; \
echo ">> $(DIST_DIR)/$$base"; \
done; \
cd $(DIST_DIR) && shopt -s nullglob && archives=(*.tar.gz *.zip) && \
if command -v sha256sum >/dev/null 2>&1; then \
sha256sum "$${archives[@]}"; \
else shasum -a 256 "$${archives[@]}"; fi > SHA256SUMS
@echo ">> $(DIST_DIR)/SHA256SUMS"
## tag: bump the patch and build a release; set V=v0.1.0 to override (needs gh)
tag: export RELEASE_VERSION = $(V)
tag:
@[[ -z "$$RELEASE_VERSION" || "$$RELEASE_VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$$ ]] || \
{ echo "usage: make tag [V=v0.1.0]" >&2; exit 1; }
@test -z "$$(git status --porcelain)" || { echo "the working tree is dirty" >&2; exit 1; }
@test "$$(git rev-parse HEAD)" = "$$(git ls-remote origin refs/heads/main | cut -f1)" || \
{ echo "push or update to origin/main before choosing its version" >&2; exit 1; }
gh workflow run version.yml --ref main -f version="$$RELEASE_VERSION"
@echo ">> version workflow requested; release archives will be built and published automatically"
## screenshots: regenerate the pictures in docs/ (needs Chrome or Chromium)
screenshots: frontend
bash docs/screenshots.sh
## frontend: compile the TypeScript UI into the Go embed directory
frontend:
ifdef HAVE_NPM
cd frontend && npm ci --no-audit --no-fund && npm run build
else
@echo ">> npm not found — building the UI in Docker"
docker run --rm \
-v "$(CURDIR)":/w -w /w/frontend \
-u $(UID_GID) \
-e npm_config_cache=/tmp/.npm -e HOME=/tmp \
$(NODE_IMAGE) \
sh -c 'npm ci --no-audit --no-fund && npm run build'
endif
@echo ">> UI built into $(DIST)"
# The executable discovers this directory next to itself. The installer can
# also run natively on macOS and Windows (Git Bash), without Python or Docker.
OCR_TARGET ?= $(if $(HAVE_GO),$(shell go env GOOS)/$(shell go env GOARCH),linux/$(if $(filter aarch64,$(UNAME_M)),arm64,amd64))
## ocr-assets: install the pinned OCR model and CPU runtime into ./bin/ocr
ocr-assets:
bash helpers/captcha/install.sh "$(OCR_TARGET)" "$(BIN_DIR)/ocr"
@touch $(BIN_DIR)/ocr/VERSIONS
$(BIN_DIR)/ocr/VERSIONS: helpers/captcha/install.sh
$(MAKE) ocr-assets
## captcha-helper: compatibility alias for ocr-assets (no external OCR process)
captcha-helper: ocr-assets
## dependencies: install yt-dlp, ffmpeg, deno and local OCR data into ./bin
dependencies: ocr-assets $(BIN_DIR)
@echo ">> yt-dlp ($(YTDLP_ASSET))"
@curl -fsSL --retry 3 -o "$(BIN_DIR)/yt-dlp.tmp" "$(YTDLP_URL)"
@chmod +x "$(BIN_DIR)/yt-dlp.tmp" && mv "$(BIN_DIR)/yt-dlp.tmp" "$(BIN_DIR)/yt-dlp"
@echo ">> ffmpeg ($(FFMPEG_ASSET))"
@tmp=$$(mktemp -d) && \
curl -fsSL --retry 3 -o "$$tmp/ffmpeg.tar.xz" "$(FFMPEG_URL)" && \
tar -xJf "$$tmp/ffmpeg.tar.xz" -C "$$tmp" --strip-components=2 \
--wildcards '*/bin/ffmpeg' '*/bin/ffprobe' && \
mv "$$tmp/ffmpeg" "$$tmp/ffprobe" "$(BIN_DIR)/" && \
rm -rf "$$tmp"
@chmod +x "$(BIN_DIR)/ffmpeg" "$(BIN_DIR)/ffprobe"
@echo ">> deno ($(DENO_ASSET))"
@# deno publishes zips and nothing else, so this one step needs unzip
@# where the others get by on tar. Say so rather than failing inside a
@# pipeline that has already downloaded 40MB.
@command -v unzip >/dev/null 2>&1 || { \
echo "unzip is required to unpack deno; install it and re-run" >&2; exit 1; }
@tmp=$$(mktemp -d) && \
curl -fsSL --retry 3 -o "$$tmp/deno.zip" "$(DENO_URL)" && \
unzip -oq "$$tmp/deno.zip" -d "$$tmp" && \
mv "$$tmp/deno" "$(BIN_DIR)/" && \
rm -rf "$$tmp"
@chmod +x "$(BIN_DIR)/deno"
@printf ' yt-dlp %s\n' "$$($(BIN_DIR)/yt-dlp --version 2>/dev/null)"
@$(BIN_DIR)/ffmpeg -version 2>/dev/null | head -1 | sed 's/^/ /'
@$(BIN_DIR)/deno --version 2>/dev/null | head -1 | sed 's/^/ /'
@echo ">> ready in $(BIN_DIR)/ — the service prefers these over PATH"
## lock: generate frontend/package-lock.json (reproducible installs)
lock:
docker run --rm \
-v "$(CURDIR)":/w -w /w/frontend \
-u $(UID_GID) \
-e npm_config_cache=/tmp/.npm -e HOME=/tmp \
$(NODE_IMAGE) \
npm install --package-lock-only --no-audit --no-fund
@echo ">> wrote frontend/package-lock.json"
## dev: run the Go API and the Vite dev server together (hot reload)
dev:
@echo ">> API on :8080, UI on http://localhost:5173"
@trap 'kill 0' EXIT INT TERM; \
( cd backend && HEAPLEACH_OCR_DIR="$(CURDIR)/$(BIN_DIR)/ocr" go run ./cmd/heapleach -debug "$(DOWNLOADS)" ) & \
( cd frontend && npm run dev ) & \
wait
## dev-backend: run only the Go API on port 8080
dev-backend:
cd backend && HEAPLEACH_OCR_DIR="$(CURDIR)/$(BIN_DIR)/ocr" go run ./cmd/heapleach -debug "$(DOWNLOADS)"
## dev-frontend: run only the Vite dev server on port 5173
dev-frontend:
cd frontend && npm run dev
## hosts: regenerate the supported-site list in README.md from the registry
hosts:
ifndef HAVE_GO
$(error Go is not installed — the host list is generated by the program itself)
endif
@cd backend && go run ./cmd/hostlist -write ../README.md
## hosts-check: fail if README.md's host list is not what the code produces
#
# Regenerated into a copy and compared against the file itself, rather than
# written in place and compared against git: the section is then checked in
# whatever state the tree is in, so a README with prose edits in progress
# does not read as a stale inventory, and CI — where the tree is the commit
# — still fails on a regeneration that was never committed.
hosts-check:
ifndef HAVE_GO
$(error Go is not installed — the host list is generated by the program itself)
endif
@set -e; tmp=$$(mktemp); trap 'rm -f "$$tmp"' EXIT; \
cp README.md "$$tmp"; \
(cd backend && go run ./cmd/hostlist -write "$$tmp" >/dev/null); \
if ! cmp -s README.md "$$tmp"; then \
echo "README.md's host list is out of date — run 'make hosts' and commit"; \
diff -u README.md "$$tmp" | head -40; exit 1; \
fi
@echo ">> host list is current"
## test: run the Go unit tests, with the race detector as CI does
test:
cd backend && go test ./... -race
## check: what CI checks, before pushing — formatting, vet, the host list, the tests
check: fmt-check vet hosts-check spdx-check test test-frontend test-captcha test-release
## spdx-check: fail if a source file does not name its license
# Every source file carries an SPDX identifier, so a legal team can approve
# the dependency by scanning rather than reading. Test fixtures are data.
spdx-check:
@set -e; missing=$$(git ls-files '*.go' '*.ts' '*.tsx' '*.css' '*.sh' '*.py' Makefile Dockerfile \
| grep -v '/testdata/' | xargs grep -L 'SPDX-License-Identifier: MIT' || true); \
if [ -n "$$missing" ]; then echo "no SPDX-License-Identifier in:"; echo "$$missing"; exit 1; fi
@echo ">> every source file names its license"
## fmt-check: fail if gofmt would change anything
fmt-check:
@set -e; unformatted=$$(cd backend && gofmt -l ./internal ./cmd); \
if [ -n "$$unformatted" ]; then echo "gofmt would change:"; echo "$$unformatted"; exit 1; fi
@echo ">> gofmt is happy"
## test-frontend: type-check and test the UI (Docker when npm is absent)
test-frontend:
ifdef HAVE_NPM
cd frontend && npm ci --no-audit --no-fund && npm run typecheck && npm test
else
docker run --rm \
-v "$(CURDIR)":/w -w /w/frontend \
-u $(UID_GID) \
-e npm_config_cache=/tmp/.npm -e HOME=/tmp \
$(NODE_IMAGE) \
sh -c 'npm ci --no-audit --no-fund >/dev/null && npm run typecheck && npm test'
endif
## test-captcha: test the in-process OCR reader with the real model (Docker fallback)
ifdef HAVE_GO
test-captcha: ocr-assets
cd backend && HEAPLEACH_OCR_DIR="$(CURDIR)/$(BIN_DIR)/ocr" CGO_ENABLED=1 go test -race -count=1 ./internal/captcha
else
test-captcha:
DOCKER_BUILDKIT=1 docker build --target captcha-test -f Dockerfile .
endif
## test-release: check image versions and manual tags against a temporary Git remote
test-release:
bash .github/scripts/test-release-tag.sh
## test-live: run the extractor tests against the real sites (needs network)
test-live:
cd backend && go test -tags live -v ./internal/extractor/
## fmt: format the Go sources
fmt:
cd backend && gofmt -w .
## vet: run go vet, over the live tests too
vet:
cd backend && go vet ./... && go vet -tags live ./...
## tidy: tidy the Go module
tidy:
cd backend && go mod tidy
$(BIN_DIR):
@mkdir -p $(BIN_DIR)
## clean: remove build output and restore the embed placeholder
clean: frontend-clean
# Only the binary: running `./bin/heapleach` from inside bin/ puts downloads
# in bin/downloads, and `rm -rf bin` would take them with it.
rm -f $(BINARY)
-rmdir $(BIN_DIR) 2>/dev/null || true
-docker rmi $(IMAGE):$(TAG) 2>/dev/null || true
frontend-clean:
rm -rf $(DIST)
@mkdir -p $(DIST)
@printf '%s\n' \
'<!doctype html>' \
'<html lang="en" data-heapleach-placeholder>' \
' <head><meta charset="utf-8" /><title>HeapLeach</title></head>' \
' <body><h1>Frontend not built</h1>' \
' <p>Run <code>make frontend</code> or <code>make build</code>.</p></body>' \
'</html>' > $(DIST)/index.html
@echo ">> restored the $(DIST) placeholder"
## distclean: clean plus node_modules and the downloaded helper tools
distclean: clean
rm -rf frontend/node_modules
rm -f $(BIN_DIR)/yt-dlp $(BIN_DIR)/ffmpeg $(BIN_DIR)/ffprobe $(BIN_DIR)/deno
@echo ">> left $(DOWNLOADS) alone; remove it by hand if you want to"