Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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"