magisk-zapret2/build.sh
loop-uh 9c3c29197c Archive the assembly root instead of a second list of its files
package_contract_assemble_package builds a tree that is exactly the
runtime manifest, and then every caller named the top-level entries
again by hand to zip them. That second list has no authority and no
way to stay in step: post-fs-data.sh was added to the manifest, the
package contract, the installer and the release build, and the CI
workflow's own copy still omitted it — so the published archive failed
package_contract_validate_zip_names on a file the contract required.

There is nothing to enumerate: the archive is the assembly root. The
release build, the CI workflow and the three packaging fixtures now
archive it whole, which removes the class of failure rather than the
instance. The test helper's store-only fallback normalizes the ./
prefix `find` adds, so both implementations produce the same names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:48:38 +03:00

277 lines
11 KiB
Shell

#!/bin/bash
##########################################################################################
# Zapret2 Root Module - Local Build Script
##########################################################################################
set -e
REQUESTED_VERSION="${1:-}"
OUTPUT_DIR="${2:-.artifacts/module}"
PRERELEASE_ID="${3:-}"
VERSION_METADATA=$(sh tools/release-version.sh)
VERSION=$(printf '%s\n' "$VERSION_METADATA" | sed -n 's/^version=//p')
VERSION_CODE=$(printf '%s\n' "$VERSION_METADATA" | sed -n 's/^version_code=//p')
if [ -z "$VERSION" ] || [ -z "$VERSION_CODE" ]; then
echo "ERROR: canonical release metadata is incomplete"
exit 1
fi
if [ -n "$REQUESTED_VERSION" ] &&
[ "${REQUESTED_VERSION#v}" != "$VERSION" ]; then
echo "ERROR: requested version '$REQUESTED_VERSION' does not match version.properties ($VERSION)"
exit 1
fi
echo "Building Zapret2 Root Module v$VERSION"
echo "=========================================="
VERSION_LABEL="v$VERSION"
if [ -n "$PRERELEASE_ID" ]; then
case "$PRERELEASE_ID" in
dev.*)
case "$PRERELEASE_ID" in
*[!0-9A-Za-z.-]*|*..*|.*|*.) echo "ERROR: invalid dev prerelease identifier"; exit 1 ;;
esac
;;
*) echo "ERROR: only dev prerelease identifiers are supported"; exit 1 ;;
esac
VERSION_LABEL="$VERSION_LABEL-$PRERELEASE_ID"
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
OUTPUT_DIR_ABS="$(cd "$OUTPUT_DIR" && pwd -P)"
# Check for binaries
MISSING_BIN=0
for arch in arm64-v8a armeabi-v7a; do
if [ ! -s "zapret2/bin/$arch/nfqws2" ] || [ -L "zapret2/bin/$arch/nfqws2" ]; then
echo "ERROR: Missing, empty, or unsafe binary for $arch"
MISSING_BIN=1
fi
done
if [ $MISSING_BIN -eq 1 ]; then
echo "Refusing to create a module ZIP without both supported ABI binaries."
exit 1
fi
# Update version in module.prop
sed -i "s/^version=.*/version=$VERSION_LABEL/" module.prop
sed -i "s/^versionCode=.*/versionCode=$VERSION_CODE/" module.prop
# Make scripts executable
chmod +x customize.sh service.sh post-fs-data.sh uninstall.sh action.sh 2>/dev/null || true
chmod +x zapret2/scripts/*.sh 2>/dev/null || true
chmod +x zapret2/scripts/lifecycle/*.sh 2>/dev/null || true
chmod 0755 system/bin/zapret2-start system/bin/zapret2-stop system/bin/zapret2-status system/bin/zapret2-restart system/bin/zapret2-full-rollback
EXPECTED_OWNER_PROTOCOL_LINE='owner_protocol|7|zapret2-firewall'
ACTUAL_OWNER_PROTOCOL_LINE="$(sed -n '2p' zapret2/runtime-manifest.tsv)"
ACTUAL_OWNER_PROTOCOL_LINE="${ACTUAL_OWNER_PROTOCOL_LINE%"$(printf '\r')"}"
[ "$ACTUAL_OWNER_PROTOCOL_LINE" = "$EXPECTED_OWNER_PROTOCOL_LINE" ] || {
echo "ERROR: incompatible owner protocol in zapret2/runtime-manifest.tsv"
exit 1
}
. ./zapret2/scripts/package-contract.sh
package_contract_apply_modes "$PWD" package || {
echo "ERROR: source tree violates runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
# Runtime machine validators deliberately accept only root-owned installed files.
# Validate content against a private root-owned package copy instead of weakening
# that runtime trust boundary for a developer-owned source checkout.
root_own_validation_tree() {
local tree="$1"
[ -d "$tree" ] && [ ! -L "$tree" ] || return 1
case "$tree" in */zapret2-package-validation.*) ;; *) return 1 ;; esac
if [ "$(id -u)" -eq 0 ]; then
chown -R 0:0 "$tree" && find "$tree" -type d -exec chmod 0755 {} +
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo -n chown -R 0:0 "$tree" &&
sudo -n find "$tree" -type d -exec chmod 0755 {} +
else
echo "ERROR: root access is required to validate the installed package ownership contract" >&2
return 1
fi
}
remove_root_validation_tree() {
local tree="${1:-}"
[ -n "$tree" ] || return 0
case "$tree" in */zapret2-package-validation.*) ;; *) return 1 ;; esac
[ -e "$tree" ] || [ -L "$tree" ] || return 0
[ -d "$tree" ] && [ ! -L "$tree" ] || return 1
if [ "$(id -u)" -eq 0 ]; then
rm -rf "$tree"
else
sudo -n rm -rf "$tree"
fi
}
PACKAGE_SOURCE_VALIDATE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/zapret2-package-validation.XXXXXX")"
trap 'remove_root_validation_tree "${PACKAGE_SOURCE_VALIDATE_ROOT:-}"' EXIT
trap 'exit 1' HUP INT TERM
package_contract_assemble_package "$PWD" "$PACKAGE_SOURCE_VALIDATE_ROOT" || {
echo "ERROR: cannot assemble source validation tree: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
root_own_validation_tree "$PACKAGE_SOURCE_VALIDATE_ROOT" || exit 1
package_contract_validate_all "$PACKAGE_SOURCE_VALIDATE_ROOT" package || {
echo "ERROR: source catalog violates runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
package_contract_validate_modes "$PACKAGE_SOURCE_VALIDATE_ROOT" package || {
echo "ERROR: source modes violate runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
remove_root_validation_tree "$PACKAGE_SOURCE_VALIDATE_ROOT" || exit 1
PACKAGE_SOURCE_VALIDATE_ROOT=""
trap - EXIT HUP INT TERM
WRAPPER_EXPECTED="$(mktemp)"
WRAPPER_ACTUAL="$(mktemp)"
ZIP_LIST="$(mktemp)"
PACKAGE_ASSEMBLY_ROOT="$(mktemp -d)"
PACKAGE_VALIDATE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/zapret2-package-validation.XXXXXX")"
cleanup_build_temps() {
rm -f "$WRAPPER_EXPECTED" "$WRAPPER_ACTUAL" "$ZIP_LIST"
[ -z "${PACKAGE_SOURCE_VALIDATE_ROOT:-}" ] || remove_root_validation_tree "$PACKAGE_SOURCE_VALIDATE_ROOT"
[ -z "${PACKAGE_ASSEMBLY_ROOT:-}" ] || rm -rf "$PACKAGE_ASSEMBLY_ROOT"
[ -z "${PACKAGE_VALIDATE_ROOT:-}" ] || remove_root_validation_tree "$PACKAGE_VALIDATE_ROOT"
}
trap cleanup_build_temps EXIT
trap 'exit 1' HUP INT TERM
# Match the immutable command-entry contract enforced by the release workflow.
test -d system/bin
test ! -L system
test ! -L system/bin
test -f zapret2/scripts/zapret-full-rollback.sh
test ! -L zapret2/scripts/zapret-full-rollback.sh
test -x zapret2/scripts/zapret-full-rollback.sh
test "$(sed -n '1p' zapret2/scripts/zapret-full-rollback.sh)" = '#!/system/bin/sh'
if LC_ALL=C grep -q "$(printf '\r')" zapret2/scripts/zapret-full-rollback.sh; then exit 1; fi
for purge_script in zapret2/scripts/lifecycle/purge-contract.sh zapret2/scripts/lifecycle/zapret-purge.sh; do
test -f "$purge_script"
test ! -L "$purge_script"
test -s "$purge_script"
test -x "$purge_script"
test "$(sed -n '1p' "$purge_script")" = '#!/system/bin/sh'
if LC_ALL=C grep -q "$(printf '\r')" "$purge_script"; then exit 1; fi
done
for command_name in start stop status restart full-rollback; do
wrapper="system/bin/zapret2-$command_name"
target="$PACKAGE_CONTRACT_ACTIVE_MODULE_DIR/zapret2/scripts/zapret-$command_name.sh"
test -f "$wrapper"
test ! -L "$wrapper"
test -x "$wrapper"
{
printf '%s\n' '#!/system/bin/sh'
printf 'exec %s "$@"\n' "$target"
} > "$WRAPPER_EXPECTED"
cmp -s "$WRAPPER_EXPECTED" "$wrapper"
done
# Create ZIP
ZIP_NAME="zapret2-magisk-$VERSION_LABEL.zip"
ZIP_PATH="$OUTPUT_DIR_ABS/$ZIP_NAME"
echo "Creating $ZIP_NAME..."
package_contract_assemble_package "$PWD" "$PACKAGE_ASSEMBLY_ROOT" || {
echo "ERROR: cannot assemble exact runtime-manifest package: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
# Never update an existing archive in place: Info-ZIP otherwise retains files
# deleted from the exact manifest-derived assembly tree.
rm -f "$ZIP_PATH"
(
cd "$PACKAGE_ASSEMBLY_ROOT"
# The assembly root is exactly what the runtime manifest declares, so the
# archive is its whole contents. Naming the files again by hand only
# created a second, silently divergent list: post-fs-data.sh shipped in the
# manifest and the package contract while every zip command still omitted
# it, and the release failed its own name check.
zip -r "$ZIP_PATH" .
)
# Fail locally on the same missing/duplicate/type/mode/content regressions that
# are release blockers in CI.
zipinfo -1 "$ZIP_PATH" | tee "$ZIP_LIST"
package_contract_validate_zip_names "$PWD" "$ZIP_LIST" || {
echo "ERROR: ZIP entries violate runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
unzip -q "$ZIP_PATH" -d "$PACKAGE_VALIDATE_ROOT"
root_own_validation_tree "$PACKAGE_VALIDATE_ROOT" || exit 1
package_contract_validate_all "$PACKAGE_VALIDATE_ROOT" package || {
echo "ERROR: extracted ZIP violates runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
package_contract_validate_exact_tree "$PACKAGE_VALIDATE_ROOT" package || {
echo "ERROR: extracted ZIP contains undeclared entries: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
package_contract_validate_modes "$PACKAGE_VALIDATE_ROOT" package || {
echo "ERROR: ZIP modes violate runtime manifest: $PACKAGE_CONTRACT_CODE $PACKAGE_CONTRACT_DETAIL"
exit 1
}
grep -Fxq 'system/bin/' "$ZIP_LIST"
for command_name in start stop status restart full-rollback; do
entry="system/bin/zapret2-$command_name"
target="$PACKAGE_CONTRACT_ACTIVE_MODULE_DIR/zapret2/scripts/zapret-$command_name.sh"
test "$(grep -Fxc "$entry" "$ZIP_LIST")" -eq 1
zipinfo -l "$ZIP_PATH" "$entry" | grep -Eq '^-rwxr-xr-x[[:space:]]'
{
printf '%s\n' '#!/system/bin/sh'
printf 'exec %s "$@"\n' "$target"
} > "$WRAPPER_EXPECTED"
unzip -p "$ZIP_PATH" "$entry" > "$WRAPPER_ACTUAL"
cmp -s "$WRAPPER_EXPECTED" "$WRAPPER_ACTUAL"
done
ROLLBACK_SCRIPT_ENTRY='zapret2/scripts/zapret-full-rollback.sh'
test "$(grep -Fxc "$ROLLBACK_SCRIPT_ENTRY" "$ZIP_LIST")" -eq 1
zipinfo -l "$ZIP_PATH" "$ROLLBACK_SCRIPT_ENTRY" | grep -Eq '^-rwxr-xr-x[[:space:]]'
unzip -p "$ZIP_PATH" "$ROLLBACK_SCRIPT_ENTRY" > "$WRAPPER_ACTUAL"
cmp -s "$ROLLBACK_SCRIPT_ENTRY" "$WRAPPER_ACTUAL"
if LC_ALL=C grep -q "$(printf '\r')" "$WRAPPER_ACTUAL"; then exit 1; fi
for PURGE_SCRIPT_ENTRY in \
zapret2/scripts/lifecycle/purge-contract.sh \
zapret2/scripts/lifecycle/zapret-purge.sh; do
test "$(grep -Fxc "$PURGE_SCRIPT_ENTRY" "$ZIP_LIST")" -eq 1
zipinfo -l "$ZIP_PATH" "$PURGE_SCRIPT_ENTRY" | grep -Eq '^-rwxr-xr-x[[:space:]]'
unzip -p "$ZIP_PATH" "$PURGE_SCRIPT_ENTRY" > "$WRAPPER_ACTUAL"
test -s "$WRAPPER_ACTUAL"
cmp -s "$PURGE_SCRIPT_ENTRY" "$WRAPPER_ACTUAL"
test "$(sed -n '1p' "$WRAPPER_ACTUAL")" = '#!/system/bin/sh'
if LC_ALL=C grep -q "$(printf '\r')" "$WRAPPER_ACTUAL"; then exit 1; fi
done
test "$(grep -Fxc 'action.sh' "$ZIP_LIST")" -eq 1
zipinfo -l "$ZIP_PATH" action.sh | grep -Eq '^-rwxr-xr-x[[:space:]]'
# The boot-time hosts publication entry point is executed by the root manager,
# so it ships with the same executable bit the other module scripts get.
test "$(grep -Fxc 'post-fs-data.sh' "$ZIP_LIST")" -eq 1
zipinfo -l "$ZIP_PATH" post-fs-data.sh | grep -Eq '^-rwxr-xr-x[[:space:]]'
if grep -Eq '^META-INF(/|$)' "$ZIP_LIST"; then
echo "ERROR: recovery flashing metadata must not be published" >&2
exit 1
fi
echo ""
echo "Build complete: $ZIP_PATH"
echo ""
# Show file size
ls -lh "$ZIP_PATH"
# Verify ZIP structure
echo ""
echo "ZIP contents:"
unzip -l "$ZIP_PATH" | head -30
cleanup_build_temps
trap - EXIT HUP INT TERM