Files
XC_VM/Makefile
T
Divarion_D b9de28f624 build: widen the Rector set, fix its cache trap, and quiet the fixerless phpcs sniffs
Rector — enable instanceOf, earlyReturn and if. Each was measured at zero changes
across the analysed tree, so they cost nothing now and only hold the line on new
code. The counts for every set that stays off are recorded next to them, with the
reason, so the decision does not have to be re-derived: typeDeclarations(319) is
the native-type footgun TYPE_AUDIT.md tracks, typeDeclarationDocblocks(86) feeds
that same footgun one step removed because `make cs-fix` derives native types from
docblocks, codingStyle(255) rewrites the `use` imports check-procedural-use depends
on, naming(132) fights the $r-prefix convention and render()-by-name views, and
privatization reads as 0 only because there are almost no final classes yet.

Makefile — `make rector` now passes --clear-cache. Rector's cache key does not
track the rule set, so after editing the config a cached run prints "Rector is
done!" and a newly enabled set looks like a no-op. The measurements above first
came back all-zero for exactly this reason.

phpcs — silence the four sniffs that were still reporting. Every one of them was
checked for a fixer first (none calls addFixable), so `make cs-fix` could never
have cleared them; what was left was 769 warnings no tool can act on:

  NoSilencedErrors(473)  each @ is either deliberate or masking a real error, so
                         they need judging one at a time, with a test each
  CyclomaticComplexity   (201 err + 190 warn) and NestingLevel (2 + 84) — already
                         ratcheted per-function by the CRAP gate, which blocks NEW
                         complexity; the sniffs only restated the old backlog
  PSR1.Files.SideEffects(22)  needs files split, which moves loading and autoload

LineLength(3480) went the same way earlier and is marked as not part of the
ratchet: it has no fixer at all, so "re-enable and fix" would mean splitting those
lines by hand.

Own rules are commented out so they read as toggles; the PSR12-inherited ones
(SideEffects, LineLength) can only be <exclude>d, which the ratchet summary now
says out loud. That summary is updated with every new toggle and its count.

`make cs` is now clean, with 85 sniffs still active — checked, because an empty
report looks the same whether nothing is wrong or everything got muted.
2026-09-22 18:20:45 +03:00

496 lines
22 KiB
Makefile

PROJECT_NAME := $(notdir $(CURDIR))
TIMESTAMP := $(shell date +%s)
TEMP_DIR := /tmp/XC_VM-$(TIMESTAMP)
MAIN_DIR = ./src
DIST_DIR = ./dist
CONFIG_DIR := ./lb_configs
TEMP_ARCHIVE_NAME := $(TIMESTAMP).tar.gz
MAIN_ARCHIVE_NAME := xc_vm.tar.gz
MAIN_ARCHIVE_INSTALLER := XC_VM.zip
LB_ARCHIVE_NAME := loadbalancer.tar.gz
# Latest published release tag — consumed only by generate_deleted_files to diff
# against the previous release. Uses `?=` (lazy) so plain builds (new/main/lb)
# never evaluate it and thus never hit the network; the GitHub API is queried
# only when generate_deleted_files actually expands LAST_TAG. Override on the CLI
# to skip the API call entirely (CI passes it this way):
# make generate_deleted_files LAST_TAG=v1.2.3
LAST_TAG ?= $(shell curl -s https://api.github.com/repos/Vateron-Media/XC_VM/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
HASH_FILE := hashes.md5
# Directories and files to exclude from archives
EXCLUDES := \
.git
# Directories to copy from MAIN to LB
# NOTE: Modules/ is intentionally excluded — all modules are MAIN-only
# (e.g. the ministra portal, whose ~50 MB of assets must not ship to LB nodes).
LB_DIRS := bin Cli config content Core Domain\
Infrastructure Public resources signals Streaming tmp vendor www
# Root-level files to copy from MAIN to LB (not inside directories)
LB_ROOT_FILES := bootstrap.php console.php service update
# Directories to remove from LB (admin-only content)
LB_DIRS_TO_REMOVE := \
bin/install \
bin/redis \
bin/nginx/conf/codes \
Domain/User \
Domain/Device \
Domain/Auth \
Public/Controllers/Admin \
Public/Controllers/Player \
Public/Controllers/Reseller \
Public/Views \
Public/assets \
Public/routes \
Core/Reference \
Core/Localization/lang
# Files to remove from LB
LB_FILES_TO_REMOVE := \
bin/maxmind/GeoLite2-City.mmdb \
Public/Controllers/Api/AdminApiController.php \
Public/Controllers/Api/ResellerRestApiController.php \
www/xplugin.php \
www/probe.php \
www/playlist.php \
www/player_api.php \
www/epg.php \
www/enigma2.php \
www/stream/auth.php \
www/admin/proxy_api.php \
www/admin/api.php \
config/rclone.conf \
Cli/Commands/MigrateCommand.php \
Cli/Commands/CacheHandlerCommand.php \
Cli/Commands/ServerInstallCommand.php \
Cli/Commands/LbInstallFlow.php \
Cli/Commands/ProxyInstallFlow.php \
Cli/CronJobs/RootMysqlCronJob.php \
Cli/CronJobs/BackupsCronJob.php \
Cli/CronJobs/CacheEngineCronJob.php \
Cli/CronJobs/EpgCronJob.php \
Cli/CronJobs/UpdateCronJob.php \
Cli/CronJobs/ProvidersCronJob.php \
Cli/CronJobs/SeriesCronJob.php \
Domain/Epg/EPG.php \
Core/Enum/Theme.php \
Core/Enum/ResellerAction.php \
Core/Enum/ClientFilter.php \
bin/nginx/conf/gzip.conf
EXCLUDE_ARGS := $(addprefix --exclude=,$(EXCLUDES))
.PHONY: new lb main lb_copy_files main_copy_files set_permissions create_archive \
lb_archive_move main_archive_move main_install_archive clean \
verify_no_lfs_pointers \
lb_delete_files_list generate_deleted_files \
phpstan phpstan-baseline cs cs-fix check-procedural-use verify-lb-archive gates \
check-vendor-prod-only dev-tools dev-clean rector rector-fix
# ─── Dev tooling ────────────────────────────────────────────────
# The committed src/vendor/ is PRODUCTION-ONLY (composer install --no-dev). The
# dev tools below (PHPStan, phpcs) are require-dev packages — install them
# into src/vendor/ once with `make dev-tools` before running phpstan / cs. CI runs
# the equivalent `composer install` step itself. They are never committed (the
# committed vendor stays prod-only — see tools/ci/check-vendor-prod-only.sh).
dev-tools:
@cd src && composer install --no-interaction
# Inverse of dev-tools: once you no longer need the checks, remove the installed
# dev libraries (PHPStan, phpcs + transitive deps) and restore the
# production-only vendor/. `composer install --no-dev` prunes vendor/ back to the
# committed prod set, so `git status` stays clean and no dev package can be staged.
dev-clean:
@cd src && composer install --no-dev --no-interaction
@# composer rewrites installed.php/.json with the root package's current git
@# reference (HEAD sha), so they churn after every commit even though the prod
@# package set is identical — restore the committed copies to keep vendor clean.
@git checkout -- src/vendor/composer/installed.php src/vendor/composer/installed.json 2>/dev/null || true
# ─── Static analysis (PHPStan) ──────────────────────────────────
PHPSTAN := src/vendor/bin/phpstan
# Run the analysis using build/phpstan.dist.neon.
phpstan:
@test -x "$(PHPSTAN)" || { echo "PHPStan not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(PHPSTAN)" analyse -c build/phpstan.dist.neon --memory-limit=2G
# Dead-code audit (on demand, NOT a CI gate): reports unused PUBLIC methods /
# properties / constants via tomasvotruba/unused-public. Expect false positives
# for dynamically-invoked code (routes, #[ListensTo], commands) — verify by hand.
phpstan-deadcode:
@test -x "$(PHPSTAN)" || { echo "PHPStan not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(PHPSTAN)" analyse -c build/phpstan-deadcode.neon --memory-limit=2G
# Freeze all current errors into build/phpstan-baseline.neon (run after a level bump).
phpstan-baseline:
@test -x "$(PHPSTAN)" || { echo "PHPStan not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(PHPSTAN)" analyse -c build/phpstan.dist.neon --memory-limit=2G --generate-baseline=build/phpstan-baseline.neon
# Regenerate tools/phpstan/constants.stub.php from the current src/ define()s.
# Run when runtime-defined constants change so the stub PHPStan reads stays
# accurate (constants.stub.php is a bootstrapFile in build/phpstan.dist.neon).
phpstan-stub:
@php tools/phpstan/gen-constants-stub.php > tools/phpstan/constants.stub.php
@echo "regenerated tools/phpstan/constants.stub.php ($$(grep -c 'define(' tools/phpstan/constants.stub.php) constants)"
# ─── Code style (PHP_CodeSniffer + Slevomat) ────────────────────
# Narrow ruleset — import/namespace hygiene only (no PSR-12 reformat). See
# build/phpcs.xml.dist. Replaced PHP-CS-Fixer because Slevomat's UnusedUses is
# precise (it does not treat a class name that merely appears in a PHPDoc
# DESCRIPTION as "used", unlike PHP-CS-Fixer's no_unused_imports).
PHPCS := src/vendor/bin/phpcs
PHPCBF := src/vendor/bin/phpcbf
# View templates (`<?`/`<?=`) are excluded in the ruleset; analysed class files
# use full `<?php`, so no short_open_tag handling is needed.
PHPCS_FLAGS := --standard=build/phpcs.xml.dist --cache=build/.phpcs-cache src
# Check only — non-zero exit on any violation. Used in CI.
cs:
@test -x "$(PHPCS)" || { echo "phpcs not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(PHPCS)" $(PHPCS_FLAGS)
# Apply fixes in place (phpcbf).
cs-fix:
@test -x "$(PHPCBF)" || { echo "phpcbf not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(PHPCBF)" $(PHPCS_FLAGS)
# ─── Automated refactoring (Rector) ─────────────────────────────
# Rector is a require-dev tool (like phpstan/phpcs); config in build/rector.php.
# See docs/en/guides/refactoring.md. Always review a `rector` dry-run diff before
# `rector-fix`, then re-run cs-fix + phpstan + tests on the result.
RECTOR := src/vendor/bin/rector
# Dry-run — report what would change, never write. Non-zero exit when changes
# are pending, so it doubles as a CI check.
#
# --clear-cache is not optional here: Rector's cache key does not track the rule
# set, so after editing build/rector.php a cached run reports "Rector is done!"
# and a newly enabled set looks like it changes nothing.
rector:
@test -x "$(RECTOR)" || { echo "Rector not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(RECTOR)" process -c build/rector.php --dry-run --clear-cache
# Apply changes in place.
rector-fix:
@test -x "$(RECTOR)" || { echo "Rector not found — run 'make dev-tools' (composer install) first."; exit 1; }
@php "$(RECTOR)" process -c build/rector.php
# ─── PSR-4 regression gates ─────────────────────────────────────
# Helper: print a variable's resolved value (consumed by CI gate scripts).
print-%:
@echo '$($*)'
# Procedural/view files must import every migrated class they use with a
# top-of-file `use` (PHP `use` is positional outside the top scope).
check-procedural-use:
@php -d short_open_tag=1 tools/ci/check_procedural_use.php
# LB archive must never contain privileged code (security blocker 1).
verify-lb-archive:
@bash tools/ci/verify-lb-archive.sh
# Assert the committed src/vendor/ is production-only (no require-dev packages).
# Dev tools (PHPStan, phpcs) are installed locally via `composer install`
# and must never be committed. Checks git-tracked files, so it is correct even
# in a CI job that has already run `composer install`.
check-vendor-prod-only:
@bash tools/ci/check-vendor-prod-only.sh
# Run every fast PSR-4 gate.
gates: check-procedural-use verify-lb-archive check-vendor-prod-only
# ─── Admin E2E (Playwright) ─────────────────────────────────────
# Browser smoke tests for the admin panel. Run against a LIVE instance:
# set XC_E2E_BASE_URL / XC_E2E_USER / XC_E2E_PASS (see tests/e2e/README.md).
.PHONY: e2e e2e-ui e2e-install
e2e-install:
cd tests/e2e && npm ci && npx playwright install --with-deps chromium
e2e:
cd tests/e2e && npx playwright test
e2e-ui:
cd tests/e2e && npx playwright test --ui
# ─── Generate deleted_files.txt from git diff ───────────────────
# Usage: make generate_deleted_files [LAST_TAG=v1.2.3]
# Writes to src/migrations/deleted_files.txt for manual review.
generate_deleted_files:
@if [ -z "$(LAST_TAG)" ]; then \
echo "[ERROR] LAST_TAG is empty — cannot detect latest release."; \
echo " Pass it manually: make generate_deleted_files LAST_TAG=v1.2.3"; \
exit 1; \
fi
@if ! git rev-parse "$(LAST_TAG)" >/dev/null 2>&1; then \
echo "[ERROR] Tag '$(LAST_TAG)' not found locally. Run: git fetch --tags"; \
exit 1; \
fi
@echo "[INFO] Generating deleted_files.txt: $(LAST_TAG)..HEAD"
@{ \
echo '# Files to delete during update (relative to MAIN_HOME)'; \
echo '# Auto-generated from: git diff $(LAST_TAG)..HEAD'; \
echo '# One file per line. Lines starting with # are comments.'; \
echo '#'; \
git diff --name-status --no-renames "$(LAST_TAG)"..HEAD -- src/ \
| awk '$$1 == "D" { sub(/^src\//, "", $$2); print $$2 }'; \
} > "$(MAIN_DIR)/migrations/deleted_files.txt"
@rCount=$$(grep -cv '^#\|^$$' "$(MAIN_DIR)/migrations/deleted_files.txt" 2>/dev/null || echo 0); \
echo "[INFO] Generated $(MAIN_DIR)/migrations/deleted_files.txt ($$rCount files)"; \
if [ "$$rCount" -gt 0 ]; then \
echo "[INFO] Files to delete:"; \
grep -v '^#' "$(MAIN_DIR)/migrations/deleted_files.txt" | grep -v '^$$'; \
else \
echo "[INFO] No deleted files detected since $(LAST_TAG)"; \
fi
# ─── MAIN targets ────────────────────────────────────────────────
# Single archive: used for both clean install and update.
# The update script (src/update) filters out excluded dirs at runtime.
main: main_copy_files stamp_release_id set_permissions verify_no_lfs_pointers create_archive main_archive_move main_install_archive clean
# ─── LoadBalancer targets ────────────────────────────────────────
lb: lb_copy_files lb_delete_files_list stamp_release_id set_permissions verify_no_lfs_pointers create_archive lb_archive_move clean
lb_copy_files:
@echo "==> [LB] Creating distribution directory: $(DIST_DIR)"
@mkdir -p ${DIST_DIR}
@echo "==> [LB] Creating temporary directory: $(TEMP_DIR)"
@mkdir -p ${TEMP_DIR}
@echo "==> [LB] Copying tracked directories from $(MAIN_DIR)"
@for lb_item in $(LB_DIRS); do \
printf " → Scanning: %s\n" "$$lb_item"; \
git ls-files | grep "^src/$$lb_item/" | while read -r file; do \
rel=$${file#src/}; \
printf " → Copying: %s\n" "$$file"; \
mkdir -p "$(TEMP_DIR)/$$(dirname $$rel)"; \
cp "$$file" "$(TEMP_DIR)/$$rel"; \
done; \
done
@echo "==> [LB] Copying root files from $(MAIN_DIR)"
@for root_file in $(LB_ROOT_FILES); do \
if git ls-files --error-unmatch "src/$$root_file" >/dev/null 2>&1; then \
cp "$(MAIN_DIR)/$$root_file" "$(TEMP_DIR)/$$root_file"; \
else \
printf " ⚠ Not tracked: %s\n" "$$root_file"; \
fi; \
done
@echo "==> [LB] Removing excluded directories"
@for dir in $(LB_DIRS_TO_REMOVE); do \
echo " → Removing directory: $$dir"; \
rm -rf "$(TEMP_DIR)/$$dir"; \
done
@echo "==> [LB] Removing excluded files"
@for file in $(LB_FILES_TO_REMOVE); do \
echo " → Removing file: $$file"; \
rm -f "$(TEMP_DIR)/$$file"; \
done
@echo "==> [LB] Copying config files"
cp "$(CONFIG_DIR)/nginx.conf" $(TEMP_DIR)/bin/nginx/conf/nginx.conf
cp "$(CONFIG_DIR)/live.conf" $(TEMP_DIR)/bin/nginx_rtmp/conf/live.conf
@echo "Remove all .gitkeep files..."
@find $(TEMP_DIR) -name .gitkeep \
-not -path "*/.git/*" \
-delete
@echo "All files gitkeep deleted"
main_copy_files:
@echo "==> [MAIN] Creating distribution directory: $(DIST_DIR)"
mkdir -p ${DIST_DIR}
@echo "==> [MAIN] Creating temporary directory: $(TEMP_DIR)"
mkdir -p $(TEMP_DIR)
@echo "==> [MAIN] Copying tracked files from $(MAIN_DIR)"
@git ls-files src | while read -r file; do \
rel=$${file#src/}; \
# printf " → Copying: %s\n" "$$file"; \
mkdir -p "$(TEMP_DIR)/$$(dirname $$rel)"; \
cp "$$file" "$(TEMP_DIR)/$$rel"; \
done
@echo "Remove all .gitkeep files..."
@find $(TEMP_DIR) -name .gitkeep \
-not -path "*/.git/*" \
-delete
@echo "All files gitkeep deleted"
# Stamp a unique per-build watermark into the staged tree for provenance / leak
# tracing. Generated per build (NOT git-tracked, one file per archive). Runtime
# exposes it as the XC_VM_BUILD_ID constant (ConstantsInitializer reads
# RELEASE_ID from the deploy root); a source/dev checkout has no file -> "dev".
stamp_release_id:
@ver=$$(sed -nE "s/.*define\('XC_VM_VERSION', '([^']+)'\).*/\1/p" $(MAIN_DIR)/Core/Config/ConstantsInitializer.php | head -n1); \
sha=$$(git rev-parse --short=10 HEAD 2>/dev/null || echo nogit); \
rnd=$$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n'); \
id="$${ver:-0}+$${sha}.$$(date -u +%Y%m%dT%H%M%SZ).$${rnd}"; \
printf '%s\n' "$$id" > "$(TEMP_DIR)/RELEASE_ID"; \
echo "==> [BUILD] Stamped RELEASE_ID: $$id"
lb_delete_files_list:
@echo "[INFO] Checking for manual deleted files list (LB-scoped)"
@if [ -f "$(MAIN_DIR)/migrations/deleted_files.txt" ]; then \
mkdir -p "$(TEMP_DIR)/migrations"; \
grep -v '^#' "$(MAIN_DIR)/migrations/deleted_files.txt" | grep -v '^$$' \
| awk -v dirs="$(LB_DIRS)" -v files="$(LB_ROOT_FILES)" ' \
BEGIN { n=split(dirs,d," "); m=split(files,f," ") } \
{ ok=0; for(i=1;i<=n;i++) if(index($$0,d[i]"/")==1){ok=1;break} \
if(!ok) for(i=1;i<=m;i++) if($$0==f[i]){ok=1;break} \
if(ok) print }' \
> "$(TEMP_DIR)/migrations/deleted_files.txt"; \
if [ -s "$(TEMP_DIR)/migrations/deleted_files.txt" ]; then \
echo "[INFO] LB files to delete on update:"; \
cat "$(TEMP_DIR)/migrations/deleted_files.txt"; \
else \
echo "[INFO] No LB-scoped deleted files found"; \
rm -f "$(TEMP_DIR)/migrations/deleted_files.txt"; \
fi; \
else \
echo "[INFO] No deleted_files.txt found — skipping"; \
fi
set_permissions:
@echo "==> Setting file and directory permissions"
# Global defaults: directories 755, regular files 644
@find $(TEMP_DIR) -type d -exec chmod 755 {} +
@find $(TEMP_DIR) -type f -exec chmod 644 {} +
# Restricted root directories (750)
@for d in backups bin config content signals; do \
chmod 0750 "$(TEMP_DIR)/$$d" 2>/dev/null || true; \
done
@chmod 0770 $(TEMP_DIR)/content/streams 2>/dev/null || true
# Executable scripts
@chmod 0750 $(TEMP_DIR)/service 2>/dev/null || true
@chmod 0750 $(TEMP_DIR)/update 2>/dev/null || true
@chmod 0750 $(TEMP_DIR)/bin/daemons.sh 2>/dev/null || true
@chmod 0755 $(TEMP_DIR)/bin/xc_fanout/run.sh 2>/dev/null || true
@chmod 0755 $(TEMP_DIR)/console.php 2>/dev/null || true
@chmod 0755 $(TEMP_DIR)/bin/guess 2>/dev/null || true
@chmod 0755 $(TEMP_DIR)/bin/yt-dlp 2>/dev/null || true
@chmod 0550 $(TEMP_DIR)/bin/network 2>/dev/null || true
@chmod 0550 $(TEMP_DIR)/bin/network.py 2>/dev/null || true
# FFmpeg executables
@find $(TEMP_DIR)/bin/ffmpeg_bin -type f \( -name 'ffmpeg' -o -name 'ffprobe' \) \
-exec chmod 0551 {} + 2>/dev/null || true
# Nginx binaries
@find $(TEMP_DIR)/bin/nginx -type d -exec chmod 750 {} + 2>/dev/null || true
@find $(TEMP_DIR)/bin/nginx -type f -exec chmod 550 {} + 2>/dev/null || true
@chmod 0755 $(TEMP_DIR)/bin/nginx/conf 2>/dev/null || true
@chmod 0600 $(TEMP_DIR)/bin/nginx/conf/server.key 2>/dev/null || true
@chmod 0750 $(TEMP_DIR)/bin/nginx_rtmp/sbin/nginx_rtmp 2>/dev/null || true
# PHP binaries
@find $(TEMP_DIR)/bin/php -type d -exec chmod 750 {} + 2>/dev/null || true
@find $(TEMP_DIR)/bin/php -type f -exec chmod 550 {} + 2>/dev/null || true
@for conf in 1.conf 2.conf 3.conf 4.conf; do \
chmod 0644 "$(TEMP_DIR)/bin/php/etc/$$conf" 2>/dev/null || true; \
done
@chmod 0551 $(TEMP_DIR)/bin/php/bin/php 2>/dev/null || true
@chmod 0551 $(TEMP_DIR)/bin/php/sbin/php-fpm 2>/dev/null || true
# Redis executable
@chmod 0755 $(TEMP_DIR)/bin/redis/redis-server 2>/dev/null || true
# Sensitive config files
@chmod 0640 $(TEMP_DIR)/config/modules.php 2>/dev/null || true
@chmod 0550 $(TEMP_DIR)/config/rclone.conf 2>/dev/null || true
# Fail the build if any staged file is still a Git LFS pointer instead of the
# real binary. This happens when the checkout did not materialise LFS objects
# (e.g. `actions/checkout` without `lfs: true`), which would otherwise ship
# 130-byte text stubs in place of ffmpeg, redis, yt-dlp, etc.
verify_no_lfs_pointers:
@echo "==> Verifying no Git LFS pointer files leaked into $(TEMP_DIR)"
@pointers=$$(grep -rlI '^version https://git-lfs.github.com/spec/v1' "$(TEMP_DIR)" 2>/dev/null || true); \
if [ -n "$$pointers" ]; then \
echo "ERROR: Git LFS pointer files found in the staged tree:"; \
echo "$$pointers" | sed 's|^$(TEMP_DIR)/| - |'; \
echo "The checkout did not fetch LFS objects. Run 'git lfs pull' or set 'lfs: true' on the CI checkout."; \
exit 1; \
fi; \
echo "OK: no LFS pointers staged"
create_archive:
@echo "==> Creating final archive: ${TEMP_ARCHIVE_NAME}"
@tar -czf ${DIST_DIR}/${TEMP_ARCHIVE_NAME} -C $(TEMP_DIR) .
lb_archive_move:
@echo "==> Moving LB archive to: ${DIST_DIR}/${LB_ARCHIVE_NAME}"
@rm -f ${DIST_DIR}/${LB_ARCHIVE_NAME}
@mv ${DIST_DIR}/${TEMP_ARCHIVE_NAME} ${DIST_DIR}/${LB_ARCHIVE_NAME}
md5sum "${DIST_DIR}/${LB_ARCHIVE_NAME}" | awk -v name="${LB_ARCHIVE_NAME}" '{print $$1, name}' >> "${DIST_DIR}/${HASH_FILE}"
main_archive_move:
@echo "==> Moving MAIN archive to: ${DIST_DIR}/${MAIN_ARCHIVE_NAME}"
@rm -f ${DIST_DIR}/${MAIN_ARCHIVE_NAME}
@mv ${DIST_DIR}/${TEMP_ARCHIVE_NAME} ${DIST_DIR}/${MAIN_ARCHIVE_NAME}
md5sum "${DIST_DIR}/${MAIN_ARCHIVE_NAME}" | awk -v name="${MAIN_ARCHIVE_NAME}" '{print $$1, name}' >> "${DIST_DIR}/${HASH_FILE}"
main_install_archive:
@echo "==> Creating installer archive: ${DIST_DIR}/${MAIN_ARCHIVE_INSTALLER}"
@rm -f ${DIST_DIR}/${MAIN_ARCHIVE_INSTALLER}
@zip -r ${DIST_DIR}/${MAIN_ARCHIVE_INSTALLER} install && zip -j ${DIST_DIR}/${MAIN_ARCHIVE_INSTALLER} ${DIST_DIR}/${MAIN_ARCHIVE_NAME}
md5sum "${DIST_DIR}/${MAIN_ARCHIVE_INSTALLER}" | awk -v name="${MAIN_ARCHIVE_INSTALLER}" '{print $$1, name}' >> "${DIST_DIR}/${HASH_FILE}"
clean:
@echo "==> Cleaning up temporary directory: $(TEMP_DIR)"
@rm -rf $(TEMP_DIR)
@echo "✅ Project build complete"
new:
@echo "==> Cleaning up distribution directory: $(DIST_DIR)"
@rm -rf $(DIST_DIR)
@echo "==> Creating distribution directory: $(DIST_DIR)"
@mkdir -p ${DIST_DIR}
# ─── Documentation (MkDocs Material + auto-translated ru) ────────────
# EDIT ENGLISH ONLY (docs/en). docs/ru is a GENERATED tree that is committed and
# refreshed by `make docs-translate` LOCALLY before a release — translation is
# deliberately kept out of CI (it is slow). CI (pages.yml) only builds the
# already-committed, already-translated tree. Never hand-edit docs/ru.
#
# make docs-venv # one-time: local venv (build + translation deps)
# make docs-serve # live preview at :8000 (builds committed en+ru)
# make docs-build # strict static build into ./build/site (what CI runs)
# make docs-translate # release step: (re)generate docs/ru from docs/en, then commit
#
# Translation engine via DOCS_TRANSLATE_PROVIDER (default: translators = free, no key):
# make docs-translate # free web engines (yandex/...)
# make docs-translate DOCS_TRANSLATE_PROVIDER=anthropic # needs ANTHROPIC_API_KEY
# make docs-translate DOCS_TRANSLATE_PROVIDER=noop # copy en (fast dry-run)
DOCS_VENV := build/docs-venv
DOCS_PY := $(DOCS_VENV)/bin/python
DOCS_TRANSLATE_PROVIDER ?= translators
.PHONY: docs-venv docs-translate docs-build docs-serve
# The venv's mkdocs binary doubles as the install stamp (built once). Installs
# both the build toolchain and the (local-only) translation deps.
$(DOCS_VENV)/bin/mkdocs:
@python3 -m venv $(DOCS_VENV)
@$(DOCS_VENV)/bin/pip install -q --upgrade pip
@$(DOCS_VENV)/bin/pip install -q -r docs/requirements.txt -r tools/docs/requirements.txt
docs-venv: $(DOCS_VENV)/bin/mkdocs
# Release-time step: regenerate the committed docs/ru from docs/en. Commit the
# result with the release. NOT part of docs-build (CI builds the committed tree).
docs-translate: $(DOCS_VENV)/bin/mkdocs
@DOCS_TRANSLATE_PROVIDER=$(DOCS_TRANSLATE_PROVIDER) $(DOCS_PY) -u tools/docs/translate.py --lang ru
docs-build: $(DOCS_VENV)/bin/mkdocs
@$(DOCS_PY) -m mkdocs build --strict
docs-serve: $(DOCS_VENV)/bin/mkdocs
@$(DOCS_PY) -m mkdocs serve