#!/usr/bin/env bash set -Eeuo pipefail # Build an offline-ready source bundle ZIP. # In addition to the normal source tree, this variant can attach a local # Gradle distribution zip and a helper script that rewrites wrapper URLs to # that local file so the bundle can be used without internet access. # # Usage: # ./bundle-offline.sh # ./bundle-offline.sh path/to/output.zip # # Expected local asset: # offline/gradle-offline.zip # or a custom path via BUNDLE_OFFLINE_GRADLE_ZIP ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT" OUT="${1:-SHiNE-bundle-offline-$(date +%Y%m%d-%H%M%S).zip}" case "$OUT" in /*) ;; *) OUT="$ROOT/$OUT" ;; esac if ! command -v zip >/dev/null 2>&1; then echo "ERROR: 'zip' is required." >&2 exit 1 fi TMP="$(mktemp -d)" LIST="$TMP/files.txt" SAFE_LIST="$TMP/safe-files.txt" STAGE="$TMP/stage" trap 'rm -rf "$TMP"' EXIT mkdir -p "$STAGE" # Paths / filenames that must never be bundled. is_denied_path() { local p="/$1" case "$p" in */.git/*|*/.git|\ */.gradle/*|*/.gradle|\ */.gradle-home/*|*/.gradle-home|\ */.idea/*|*/.idea|\ */.vscode/*|*/.vscode|\ */node_modules/*|*/node_modules|\ */target/*|*/target|\ */build/*|*/build|\ */out/*|*/out|\ */bin/*|*/bin|\ */logs/*|*/logs|\ */data/*|*/data|\ */test-ledger/*|*/test-ledger|\ */.anchor/*|*/.anchor|\ */.yarn/*|*/.yarn|\ */.vendor/*|*/.vendor|\ */.agents/*|*/.agents|\ */.codex/*|*/.codex|\ */.claude/*|*/.claude|\ */deploy/backup/archive/*|\ */scripts/*/runs/*|\ */scripts/*/keypairs/*|\ */keys/*|\ */.git-local-backup/*|\ */SHiNE-bundle-*.zip|\ */bundle-offline*.zip) return 0 ;; esac local base="${p##*/}" local lower lower="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')" case "$lower" in .env|.env.*|\ .debug-token|\ .npmrc|.pypirc|.netrc|\ credentials|credentials.*|\ secrets|secrets.*|\ secret|secret.*|\ id_rsa|id_dsa|id_ecdsa|id_ed25519|\ *.pem|*.key|*.p12|*.pfx|*.jks|*.keystore|\ *keypair*.json|\ service-account*.json|\ firebase-adminsdk*.json|\ google-services.json|\ validator.log) return 0 ;; esac case "$lower" in *.class|*.jar|*.war|*.ear|*.o|*.a|*.so|*.dll|*.dylib|\ *.elf|*.map|*.uf2|*.bin|*.merged.bin|\ *.log|*.bak|*.bak.png|*.tmp|*.swp|*.swo|\ .ds_store) return 0 ;; esac return 1 } find_offline_gradle_zip() { local candidate="${BUNDLE_OFFLINE_GRADLE_ZIP:-}" if [[ -n "$candidate" && -f "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi for candidate in \ "$ROOT/offline/gradle-offline.zip" \ "$ROOT/offline/gradle-8.14-bin.zip" \ "$ROOT/offline/gradle.zip" do if [[ -f "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done return 1 } create_offline_helper() { local zip_name="$1" local helper="$STAGE/offline/prepare-local-gradle.sh" local readme="$STAGE/offline/README.txt" mkdir -p "$STAGE/offline" cat > "$helper" <&2 exit 1 fi ABS_ZIP="\$(cd -- "\$(dirname -- "\$ZIP_PATH")" && pwd -P)/\$(basename -- "\$ZIP_PATH")" ESCAPED_ABS_ZIP="\${ABS_ZIP//\\\\/\\\\\\\\}" ESCAPED_ABS_ZIP="\${ESCAPED_ABS_ZIP//&/\\\\&}" ESCAPED_ABS_ZIP="\${ESCAPED_ABS_ZIP//|/\\\\|}" while IFS= read -r props; do [[ -f "\$props" ]] || continue cp -p "\$props" "\$props.bak" sed -i -e "s|^distributionUrl=.*\$|distributionUrl=file://\$ESCAPED_ABS_ZIP|" "\$props" done < <(find "\$ROOT" -path '*/gradle/wrapper/gradle-wrapper.properties' -type f | sort) cat <<'MSG' Gradle wrapper URLs rewritten to the local offline zip. Run now: ./gradlew --offline test MSG EOF chmod +x "$helper" cat > "$readme" </dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$ROOT" ls-files -co --exclude-standard -z > "$TMP/files.z" else find "$ROOT" -type f -print0 > "$TMP/files.z" fi # Convert to project-relative paths and enforce hard deny rules. : > "$LIST" while IFS= read -r -d '' f; do if [[ "$f" = /* ]]; then rel="${f#"$ROOT"/}" else rel="$f" fi [[ "$rel" == "$OUT" ]] && continue [[ -z "$rel" ]] && continue if is_denied_path "$rel"; then continue fi printf '%s\n' "$rel" >> "$LIST" done < "$TMP/files.z" sort -u "$LIST" -o "$LIST" # Always include Gradle wrapper bootstrap, even though generic JARs are denied. for wrapper_jar in \ 'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' \ 'SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.jar' do if [[ -f "$ROOT/$wrapper_jar" ]] && ! grep -Fxq "$wrapper_jar" "$LIST"; then printf '%s\n' "$wrapper_jar" >> "$LIST" fi done sort -u "$LIST" -o "$LIST" # Content scan: fail closed on common credential/private-key patterns. # We scan only text-ish files; grep -I skips binary data. SECRET_RE='-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|(^|[^A-Za-z0-9])(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\'']?[^${[:space:]]{][^[:space:]]{7,}' : > "$SAFE_LIST" found_secret=0 while IFS= read -r rel; do [[ -f "$ROOT/$rel" ]] || continue # Files that contain examples/templates can legitimately mention secret keys # with placeholders. They are scanned too, but placeholder-looking values # are less likely to match the regex above. if LC_ALL=C grep -IEnq "$SECRET_RE" "$ROOT/$rel" 2>/dev/null; then echo "BLOCKED: possible secret in $rel" >&2 LC_ALL=C grep -IEn "$SECRET_RE" "$ROOT/$rel" 2>/dev/null \ | sed -E 's/(:[[:space:]]*).*/\1[REDACTED]/' \ | head -n 3 >&2 || true found_secret=1 continue fi printf '%s\n' "$rel" >> "$SAFE_LIST" done < "$LIST" if (( found_secret != 0 )); then echo >&2 echo "Bundle NOT created because possible secrets were detected." >&2 echo "Move secrets to ignored/local files or adjust the scanner only after review." >&2 exit 2 fi if [[ ! -s "$SAFE_LIST" ]]; then echo "ERROR: no files left to bundle." >&2 exit 3 fi OFFLINE_ZIP_SRC="" OFFLINE_ZIP_NAME="" if OFFLINE_ZIP_SRC="$(find_offline_gradle_zip)"; then OFFLINE_ZIP_NAME="gradle-offline.zip" else echo "ERROR: offline Gradle zip not found." >&2 echo "Place it at ./offline/gradle-offline.zip or set BUNDLE_OFFLINE_GRADLE_ZIP." >&2 echo "The bundle is not created because this variant is meant to be offline-ready." >&2 exit 4 fi rm -rf "$STAGE" mkdir -p "$STAGE" while IFS= read -r rel; do src="$ROOT/$rel" dst="$STAGE/$rel" mkdir -p "$(dirname -- "$dst")" cp -p "$src" "$dst" done < "$SAFE_LIST" mkdir -p "$STAGE/offline" cp -p "$OFFLINE_ZIP_SRC" "$STAGE/offline/$OFFLINE_ZIP_NAME" create_offline_helper "$OFFLINE_ZIP_NAME" rm -f -- "$OUT" ( cd "$STAGE" find . -type f -print | sort | zip -q -9 "$OUT" -@ ) echo "Created: $OUT" echo "Files: $(cd "$STAGE" && find . -type f | wc -l | tr -d ' ')" echo "Size: $(du -h "$OUT" | awk '{print $1}')"