Makefile: auto-bootstrap venv, fix clean targets
- Add ensure-venv target that auto-creates venv and installs deps on first run of dev/test/lint/i18n targets - PYTHON/PIP now resolve to venv automatically when available - clean: add release artefacts (yt-local/, python/, zips, get-pip.py, vc15) - distclean: reserved for venv removal - Fix $$ escaping for shell variables in Make recipes - Sanitize LANG_CODE input against shell injection - Fix test-cov (check python module, not command), info (shell vs Make expansion) - lint/format use venv python instead of system command -v - i18n-workflow uses native Make dependencies instead of recursive make
This commit is contained in:
215
Makefile
215
Makefile
@@ -1,57 +1,88 @@
|
|||||||
# yt-local Makefile
|
# yt-local Makefile
|
||||||
# Automated tasks for development, translations, and maintenance
|
# Automated tasks for development, translations, and maintenance
|
||||||
|
|
||||||
.PHONY: help install dev clean test i18n-extract i18n-init i18n-update i18n-compile i18n-stats i18n-clean setup-dev lint format backup restore
|
.PHONY: help install dev clean test i18n-extract i18n-init i18n-update \
|
||||||
|
i18n-compile i18n-stats i18n-clean setup-dev lint format backup \
|
||||||
|
restore distclean info check-deps run test-cov i18n-workflow \
|
||||||
|
ensure-venv
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
PYTHON := python3
|
SYSTEM_PYTHON := python3
|
||||||
PIP := pip3
|
LANG_CODE ?= es
|
||||||
LANG_CODE ?= es
|
VENV_DIR := venv
|
||||||
VENV_DIR := venv
|
PROJECT_NAME := yt-local
|
||||||
PROJECT_NAME := yt-local
|
|
||||||
|
# Use venv python/pip when the venv exists, system otherwise
|
||||||
|
VENV_PYTHON := $(VENV_DIR)/bin/python
|
||||||
|
VENV_PIP := $(VENV_DIR)/bin/pip
|
||||||
|
PYTHON = $(if $(wildcard $(VENV_PYTHON)),$(VENV_PYTHON),$(SYSTEM_PYTHON))
|
||||||
|
PIP = $(if $(wildcard $(VENV_PIP)),$(VENV_PIP),$(SYSTEM_PYTHON) -m pip)
|
||||||
|
|
||||||
|
# Patterns for release artefacts (generate_release.py)
|
||||||
|
RELEASE_DIR := yt-local
|
||||||
|
RELEASE_GLOBS := yt-local-*.zip python-dist-*.zip
|
||||||
|
RELEASE_DOWNLOADS := get-pip.py vc15_*.7z
|
||||||
|
PYTHON_DIST_DIR := python
|
||||||
|
|
||||||
|
# Validate LANG_CODE: only allow letters and underscore, 2-5 chars (e.g. es, pt_BR)
|
||||||
|
# Guards against shell injection via make i18n-init LANG_CODE="$(malicious)"
|
||||||
|
LANG_CODE_SAFE := $(shell echo '$(LANG_CODE)' | grep -xE '[a-zA-Z_]{2,5}' || echo '')
|
||||||
|
|
||||||
|
## Help -----------------------------------------------------------------------
|
||||||
|
|
||||||
## Help
|
|
||||||
help: ## Show this help message
|
help: ## Show this help message
|
||||||
@echo "$(PROJECT_NAME) - Available tasks:"
|
@echo "$(PROJECT_NAME) - Available tasks:"
|
||||||
@echo ""
|
@echo ""
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
|
||||||
|
awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Examples:"
|
@echo "Examples:"
|
||||||
@echo " make install # Install dependencies"
|
@echo " make install # Install dependencies"
|
||||||
@echo " make dev # Run development server"
|
@echo " make dev # Run development server"
|
||||||
@echo " make i18n-extract # Extract strings for translation"
|
@echo " make i18n-extract # Extract strings for translation"
|
||||||
@echo " make i18n-init LANG_CODE=fr # Initialize French"
|
@echo " make i18n-init LANG_CODE=fr # Initialize French"
|
||||||
@echo " make lint # Check code style"
|
@echo " make lint # Check code style"
|
||||||
|
|
||||||
## Installation and Setup
|
## Venv bootstrap (internal) --------------------------------------------------
|
||||||
install: ## Install project dependencies
|
|
||||||
|
ensure-venv:
|
||||||
|
@if [ ! -f "$(VENV_PYTHON)" ]; then \
|
||||||
|
echo "[INFO] Creating virtual environment in $(VENV_DIR)..."; \
|
||||||
|
$(SYSTEM_PYTHON) -m venv $(VENV_DIR); \
|
||||||
|
if [ -f requirements-dev.txt ]; then \
|
||||||
|
echo "[INFO] Installing dependencies (dev)..."; \
|
||||||
|
$(VENV_DIR)/bin/pip install -r requirements-dev.txt; \
|
||||||
|
else \
|
||||||
|
echo "[INFO] Installing dependencies..."; \
|
||||||
|
$(VENV_DIR)/bin/pip install -r requirements.txt; \
|
||||||
|
fi; \
|
||||||
|
echo "[SUCCESS] Virtual environment ready"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Installation and Setup -----------------------------------------------------
|
||||||
|
|
||||||
|
install: ensure-venv ## Install/update project dependencies
|
||||||
@echo "[INFO] Installing dependencies..."
|
@echo "[INFO] Installing dependencies..."
|
||||||
$(PIP) install -r requirements.txt
|
$(PIP) install -r requirements.txt
|
||||||
@echo "[SUCCESS] Dependencies installed"
|
@echo "[SUCCESS] Dependencies installed"
|
||||||
|
|
||||||
setup-dev: ## Complete development setup
|
setup-dev: ensure-venv ## Install dev dependencies (tests, linting)
|
||||||
@echo "[INFO] Setting up development environment..."
|
@echo "[INFO] Installing dev dependencies..."
|
||||||
$(PYTHON) -m venv $(VENV_DIR)
|
$(PIP) install -r requirements-dev.txt
|
||||||
./$(VENV_DIR)/bin/pip install -r requirements.txt
|
@echo "[SUCCESS] Dev dependencies installed"
|
||||||
@echo "[SUCCESS] Virtual environment created in $(VENV_DIR)"
|
|
||||||
@echo "[INFO] Activate with: source $(VENV_DIR)/bin/activate"
|
|
||||||
|
|
||||||
requirements: ## Update and install requirements
|
## Development ----------------------------------------------------------------
|
||||||
@echo "[INFO] Installing/updating requirements..."
|
|
||||||
$(PIP) install --upgrade pip
|
|
||||||
$(PIP) install -r requirements.txt
|
|
||||||
@echo "[SUCCESS] Requirements installed"
|
|
||||||
|
|
||||||
## Development
|
dev: ensure-venv ## Run development server
|
||||||
dev: ## Run development server
|
|
||||||
@echo "[INFO] Starting development server..."
|
@echo "[INFO] Starting development server..."
|
||||||
@echo "[INFO] Server available at: http://localhost:9010"
|
@echo "[INFO] Server available at: http://localhost:9010"
|
||||||
$(PYTHON) server.py
|
$(PYTHON) server.py
|
||||||
|
|
||||||
run: dev ## Alias for dev
|
run: dev ## Alias for dev
|
||||||
|
|
||||||
## Testing
|
## Testing --------------------------------------------------------------------
|
||||||
test: ## Run tests
|
|
||||||
|
test: ensure-venv ## Run tests
|
||||||
@echo "[INFO] Running tests..."
|
@echo "[INFO] Running tests..."
|
||||||
@if [ -d "tests" ]; then \
|
@if [ -d "tests" ]; then \
|
||||||
$(PYTHON) -m pytest -v; \
|
$(PYTHON) -m pytest -v; \
|
||||||
@@ -59,32 +90,35 @@ test: ## Run tests
|
|||||||
echo "[WARN] No tests directory found"; \
|
echo "[WARN] No tests directory found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-cov: ## Run tests with coverage
|
test-cov: ensure-venv ## Run tests with coverage
|
||||||
@echo "[INFO] Running tests with coverage..."
|
@echo "[INFO] Running tests with coverage..."
|
||||||
@if command -v pytest-cov >/dev/null 2>&1; then \
|
@$(PYTHON) -c "import pytest_cov" 2>/dev/null && \
|
||||||
$(PYTHON) -m pytest -v --cov=$(PROJECT_NAME) --cov-report=html; \
|
$(PYTHON) -m pytest -v --cov=youtube --cov-report=html || \
|
||||||
else \
|
echo "[WARN] pytest-cov not installed. Run: make setup-dev"
|
||||||
echo "[WARN] pytest-cov not installed. Run: pip install pytest-cov"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
## Internationalization (i18n)
|
## Internationalization (i18n) ------------------------------------------------
|
||||||
i18n-extract: ## Extract strings for translation
|
|
||||||
|
i18n-extract: ensure-venv ## Extract strings for translation
|
||||||
@echo "[INFO] Extracting strings for translation..."
|
@echo "[INFO] Extracting strings for translation..."
|
||||||
$(PYTHON) manage_translations.py extract
|
$(PYTHON) manage_translations.py extract
|
||||||
@echo "[SUCCESS] Strings extracted to translations/messages.pot"
|
@echo "[SUCCESS] Strings extracted to translations/messages.pot"
|
||||||
|
|
||||||
i18n-init: ## Initialize new language (use LANG_CODE=xx)
|
i18n-init: ensure-venv ## Initialize new language (use LANG_CODE=xx)
|
||||||
@echo "[INFO] Initializing language: $(LANG_CODE)"
|
@if [ -z "$(LANG_CODE_SAFE)" ]; then \
|
||||||
$(PYTHON) manage_translations.py init $(LANG_CODE)
|
echo "[ERROR] Invalid LANG_CODE='$(LANG_CODE)'. Use 2-5 letters (e.g. es, pt_BR)."; \
|
||||||
@echo "[SUCCESS] Language $(LANG_CODE) initialized"
|
exit 1; \
|
||||||
@echo "[INFO] Edit: translations/$(LANG_CODE)/LC_MESSAGES/messages.po"
|
fi
|
||||||
|
@echo "[INFO] Initializing language: $(LANG_CODE_SAFE)"
|
||||||
|
$(PYTHON) manage_translations.py init $(LANG_CODE_SAFE)
|
||||||
|
@echo "[SUCCESS] Language $(LANG_CODE_SAFE) initialized"
|
||||||
|
@echo "[INFO] Edit: translations/$(LANG_CODE_SAFE)/LC_MESSAGES/messages.po"
|
||||||
|
|
||||||
i18n-update: ## Update existing translations
|
i18n-update: ensure-venv ## Update existing translations
|
||||||
@echo "[INFO] Updating existing translations..."
|
@echo "[INFO] Updating existing translations..."
|
||||||
$(PYTHON) manage_translations.py update
|
$(PYTHON) manage_translations.py update
|
||||||
@echo "[SUCCESS] Translations updated"
|
@echo "[SUCCESS] Translations updated"
|
||||||
|
|
||||||
i18n-compile: ## Compile translations to binary .mo files
|
i18n-compile: ensure-venv ## Compile translations to binary .mo files
|
||||||
@echo "[INFO] Compiling translations..."
|
@echo "[INFO] Compiling translations..."
|
||||||
$(PYTHON) manage_translations.py compile
|
$(PYTHON) manage_translations.py compile
|
||||||
@echo "[SUCCESS] Translations compiled"
|
@echo "[SUCCESS] Translations compiled"
|
||||||
@@ -98,7 +132,7 @@ i18n-stats: ## Show translation statistics
|
|||||||
po_file="$$lang_dir/LC_MESSAGES/messages.po"; \
|
po_file="$$lang_dir/LC_MESSAGES/messages.po"; \
|
||||||
if [ -f "$$po_file" ]; then \
|
if [ -f "$$po_file" ]; then \
|
||||||
total=$$(grep -c "^msgid " "$$po_file" 2>/dev/null || echo "0"); \
|
total=$$(grep -c "^msgid " "$$po_file" 2>/dev/null || echo "0"); \
|
||||||
translated=$$(grep -c "^msgstr \"[^\"]\+\"" "$$po_file" 2>/dev/null || echo "0"); \
|
translated=$$(grep -c '^msgstr "[^"]' "$$po_file" 2>/dev/null || echo "0"); \
|
||||||
fuzzy=$$(grep -c "^#, fuzzy" "$$po_file" 2>/dev/null || echo "0"); \
|
fuzzy=$$(grep -c "^#, fuzzy" "$$po_file" 2>/dev/null || echo "0"); \
|
||||||
if [ "$$total" -gt 0 ]; then \
|
if [ "$$total" -gt 0 ]; then \
|
||||||
percent=$$((translated * 100 / total)); \
|
percent=$$((translated * 100 / total)); \
|
||||||
@@ -106,50 +140,44 @@ i18n-stats: ## Show translation statistics
|
|||||||
else \
|
else \
|
||||||
echo " [STAT] $$lang: No translations yet"; \
|
echo " [STAT] $$lang: No translations yet"; \
|
||||||
fi; \
|
fi; \
|
||||||
fi \
|
fi; \
|
||||||
fi \
|
fi; \
|
||||||
done
|
done
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
i18n-clean: ## Clean compiled translation files
|
i18n-clean: ## Clean compiled translation files (.mo only)
|
||||||
@echo "[INFO] Cleaning compiled .mo files..."
|
@echo "[INFO] Cleaning compiled .mo files..."
|
||||||
find translations/ -name "*.mo" -delete
|
find translations/ -name "*.mo" -delete 2>/dev/null || true
|
||||||
@echo "[SUCCESS] .mo files removed"
|
@echo "[SUCCESS] .mo files removed"
|
||||||
|
|
||||||
i18n-workflow: ## Complete workflow: extract → update → compile
|
i18n-workflow: i18n-extract i18n-update i18n-compile i18n-stats ## Complete workflow: extract → update → compile
|
||||||
@echo "[INFO] Running complete translation workflow..."
|
|
||||||
@make i18n-extract
|
|
||||||
@make i18n-update
|
|
||||||
@make i18n-compile
|
|
||||||
@make i18n-stats
|
|
||||||
@echo "[SUCCESS] Translation workflow completed"
|
@echo "[SUCCESS] Translation workflow completed"
|
||||||
|
|
||||||
## Code Quality
|
## Code Quality ---------------------------------------------------------------
|
||||||
lint: ## Check code with flake8
|
|
||||||
|
lint: ensure-venv ## Check code with flake8
|
||||||
@echo "[INFO] Checking code style..."
|
@echo "[INFO] Checking code style..."
|
||||||
@if command -v flake8 >/dev/null 2>&1; then \
|
@$(PYTHON) -c "import flake8" 2>/dev/null && \
|
||||||
flake8 youtube/ --max-line-length=120 --ignore=E501,W503,E402 --exclude=youtube/ytdlp_service.py,youtube/ytdlp_integration.py,youtube/ytdlp_proxy.py; \
|
$(PYTHON) -m flake8 youtube/ --max-line-length=120 --ignore=E501,W503,E402 \
|
||||||
echo "[SUCCESS] Code style check passed"; \
|
--exclude=youtube/ytdlp_service.py,youtube/ytdlp_integration.py,youtube/ytdlp_proxy.py && \
|
||||||
else \
|
echo "[SUCCESS] Code style check passed" || \
|
||||||
echo "[WARN] flake8 not installed (pip install flake8)"; \
|
echo "[WARN] flake8 not installed. Run: make setup-dev"
|
||||||
fi
|
|
||||||
|
|
||||||
format: ## Format code with black (if available)
|
format: ensure-venv ## Format code with black (if available)
|
||||||
@echo "[INFO] Formatting code..."
|
@echo "[INFO] Formatting code..."
|
||||||
@if command -v black >/dev/null 2>&1; then \
|
@$(PYTHON) -c "import black" 2>/dev/null && \
|
||||||
black youtube/ --line-length=120 --exclude='ytdlp_.*\.py'; \
|
$(PYTHON) -m black youtube/ --line-length=120 --exclude='ytdlp_.*\.py' && \
|
||||||
echo "[SUCCESS] Code formatted"; \
|
echo "[SUCCESS] Code formatted" || \
|
||||||
else \
|
echo "[WARN] black not installed. Run: make setup-dev"
|
||||||
echo "[WARN] black not installed (pip install black)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
check-deps: ## Check installed dependencies
|
check-deps: ensure-venv ## Check installed dependencies
|
||||||
@echo "[INFO] Checking dependencies..."
|
@echo "[INFO] Checking dependencies..."
|
||||||
@$(PYTHON) -c "import flask_babel; print('[OK] Flask-Babel:', flask_babel.__version__)" 2>/dev/null || echo "[ERROR] Flask-Babel not installed"
|
@$(PYTHON) -c "import flask_babel; print('[OK] Flask-Babel:', flask_babel.__version__)" 2>/dev/null || echo "[ERROR] Flask-Babel not installed"
|
||||||
@$(PYTHON) -c "import flask; print('[OK] Flask:', flask.__version__)" 2>/dev/null || echo "[ERROR] Flask not installed"
|
@$(PYTHON) -c "import flask; print('[OK] Flask:', flask.__version__)" 2>/dev/null || echo "[ERROR] Flask not installed"
|
||||||
@$(PYTHON) -c "import yt_dlp; print('[OK] yt-dlp:', yt_dlp.__version__)" 2>/dev/null || echo "[ERROR] yt-dlp not installed"
|
@$(PYTHON) -c "import yt_dlp; print('[OK] yt-dlp:', yt_dlp.__version__)" 2>/dev/null || echo "[ERROR] yt-dlp not installed"
|
||||||
|
|
||||||
## Maintenance
|
## Maintenance ----------------------------------------------------------------
|
||||||
|
|
||||||
backup: ## Create translations backup
|
backup: ## Create translations backup
|
||||||
@echo "[INFO] Creating translations backup..."
|
@echo "[INFO] Creating translations backup..."
|
||||||
@timestamp=$$(date +%Y%m%d_%H%M%S); \
|
@timestamp=$$(date +%Y%m%d_%H%M%S); \
|
||||||
@@ -158,24 +186,39 @@ backup: ## Create translations backup
|
|||||||
echo "[SUCCESS] Backup created: translations_backup_$$timestamp.tar.gz"; \
|
echo "[SUCCESS] Backup created: translations_backup_$$timestamp.tar.gz"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
restore: ## Restore translations from backup
|
restore: ## Restore translations from latest backup
|
||||||
@echo "[INFO] Restoring translations from backup..."
|
@echo "[INFO] Restoring translations from backup..."
|
||||||
@if ls translations_backup_*.tar.gz 1>/dev/null 2>&1; then \
|
@latest_backup=$$(find . -maxdepth 1 -name 'translations_backup_*.tar.gz' -print0 | \
|
||||||
latest_backup=$$(ls -t translations_backup_*.tar.gz | head -1); \
|
xargs -0 ls -t 2>/dev/null | head -1); \
|
||||||
|
if [ -n "$$latest_backup" ]; then \
|
||||||
tar -xzf "$$latest_backup"; \
|
tar -xzf "$$latest_backup"; \
|
||||||
echo "[SUCCESS] Restored from: $$latest_backup"; \
|
echo "[SUCCESS] Restored from: $$latest_backup"; \
|
||||||
else \
|
else \
|
||||||
echo "[ERROR] No backup files found"; \
|
echo "[ERROR] No backup files found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
clean: ## Clean temporary files and caches
|
## Cleanup --------------------------------------------------------------------
|
||||||
|
|
||||||
|
clean: ## Clean temporary files, caches, and release artefacts
|
||||||
@echo "[INFO] Cleaning temporary files..."
|
@echo "[INFO] Cleaning temporary files..."
|
||||||
|
# Python byte-code and caches
|
||||||
find . -type f -name "*.pyc" -delete
|
find . -type f -name "*.pyc" -delete
|
||||||
find . -type d -name "__pycache__" -delete
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
# Compiled translation files (project-wide)
|
||||||
find . -type f -name "*.mo" -delete
|
find . -type f -name "*.mo" -delete
|
||||||
find . -type d -name ".pytest_cache" -delete
|
# Pytest cache and coverage data
|
||||||
|
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
|
||||||
find . -type f -name ".coverage" -delete
|
find . -type f -name ".coverage" -delete
|
||||||
find . -type d -name "htmlcov" -delete
|
# Misc temporary / backup artefacts
|
||||||
|
find . -type f \( -name "*.tmp" -o -name "*.bak" -o -name "*.log" \) -delete
|
||||||
|
find . -type d -name "*.cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
# Checksum files
|
||||||
|
find . -type f \( -name "*.sha512sum" -o -name "*.sha256sum" -o -name "*.sha1sum" \
|
||||||
|
-o -name "*.md5sum" -o -name "*.b2sum" \) -delete
|
||||||
|
# Release artefacts (generate_release.py)
|
||||||
|
rm -rf $(RELEASE_DIR)/ $(PYTHON_DIST_DIR)/
|
||||||
|
rm -f $(RELEASE_GLOBS) $(RELEASE_DOWNLOADS)
|
||||||
@echo "[SUCCESS] Temporary files removed"
|
@echo "[SUCCESS] Temporary files removed"
|
||||||
|
|
||||||
distclean: clean ## Clean everything including venv
|
distclean: clean ## Clean everything including venv
|
||||||
@@ -183,20 +226,20 @@ distclean: clean ## Clean everything including venv
|
|||||||
rm -rf $(VENV_DIR)
|
rm -rf $(VENV_DIR)
|
||||||
@echo "[SUCCESS] Complete cleanup done"
|
@echo "[SUCCESS] Complete cleanup done"
|
||||||
|
|
||||||
## Project Information
|
## Project Information --------------------------------------------------------
|
||||||
info: ## Show project information
|
|
||||||
|
info: ensure-venv ## Show project information
|
||||||
@echo "[INFO] $(PROJECT_NAME) - Project information:"
|
@echo "[INFO] $(PROJECT_NAME) - Project information:"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " [INFO] Directory: $$(pwd)"
|
@echo " [INFO] Directory: $$(pwd)"
|
||||||
@echo " [INFO] Python: $$($(PYTHON) --version)"
|
@echo " [INFO] Python: $$($(PYTHON) --version 2>&1)"
|
||||||
@echo " [INFO] Pip: $$($(PIP) --version | cut -d' ' -f1-2)"
|
@echo " [INFO] Pip: $$($(PIP) --version 2>&1 | cut -d' ' -f1-2)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " [INFO] Configured languages:"
|
@echo " [INFO] Configured languages:"
|
||||||
@for lang_dir in translations/*/; do \
|
@for lang_dir in translations/*/; do \
|
||||||
if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \
|
if [ -d "$$lang_dir" ] && [ "$$lang_dir" != "translations/*/" ]; then \
|
||||||
lang=$$(basename "$$lang_dir"); \
|
echo " - $$(basename $$lang_dir)"; \
|
||||||
echo " - $$lang"; \
|
fi; \
|
||||||
fi \
|
|
||||||
done
|
done
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " [INFO] Main files:"
|
@echo " [INFO] Main files:"
|
||||||
|
|||||||
Reference in New Issue
Block a user