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>
277 lines
11 KiB
Shell
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
|