Optimize Android APK CI: fix staging flag, add caching, faster runner (#429)
This commit is contained in:
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
build-apk:
|
||||
name: Build Staging APK
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
needs: [check-team]
|
||||
# Run when: push/dispatch (not a PR, check-team was skipped), or PR where
|
||||
# the "apk" label is present and actor is a contributor.
|
||||
@@ -55,12 +55,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
@@ -69,11 +72,6 @@ jobs:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
|
||||
|
||||
- name: Cache Flutter dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -84,7 +82,22 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pub-cache-
|
||||
|
||||
- name: Cache pre-built Android native libraries
|
||||
id: jnilibs-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: android/app/src/main/jniLibs
|
||||
key: ${{ runner.os }}-jnilibs-${{ hashFiles('rust/Cargo.toml', 'rust/Cargo.lock', 'rust/src/**', 'scripts/build_android.sh', 'scripts/build_openssl_android.sh', 'scripts/setup_android_config.sh') }}
|
||||
|
||||
# ── Rust build steps (skipped when native libs are cached) ──
|
||||
- name: Setup Rust
|
||||
if: steps.jnilibs-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
if: steps.jnilibs-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
@@ -95,28 +108,52 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-android-
|
||||
|
||||
- name: Cache OpenSSL for Android
|
||||
if: steps.jnilibs-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: rust/target/openssl/install
|
||||
key: ${{ runner.os }}-openssl-android-${{ hashFiles('scripts/build_openssl_android.sh') }}
|
||||
|
||||
- name: Cache Android SDK & NDK
|
||||
id: android-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/local/lib/android/sdk/ndk/27.0.12077973
|
||||
/usr/local/lib/android/sdk/build-tools
|
||||
/usr/local/lib/android/sdk/platforms
|
||||
key: ${{ runner.os }}-android-ndk-27.0.12077973
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android NDK
|
||||
run: |
|
||||
echo "y" | sdkmanager --install "ndk;27.0.12077973"
|
||||
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV
|
||||
if: steps.android-cache.outputs.cache-hit != 'true'
|
||||
run: echo "y" | sdkmanager --install "ndk;27.0.12077973"
|
||||
|
||||
- name: Set NDK environment
|
||||
if: steps.jnilibs-cache.outputs.cache-hit != 'true'
|
||||
run: echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Rust libraries for Android
|
||||
if: steps.jnilibs-cache.outputs.cache-hit != 'true'
|
||||
run: ./scripts/build_android.sh
|
||||
|
||||
- name: Build staging APK
|
||||
run: flutter build apk --flavor staging --split-per-abi
|
||||
run: flutter build apk --flavor staging --split-per-abi --dart-define=APP_FLAVOR=staging --build-number=${{ github.run_number }}
|
||||
|
||||
- name: Set artifact name
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
echo "ARTIFACT_SUFFIX=pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
echo "ARTIFACT_SUFFIX=pr-${PR_NUMBER}-${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF_NAME//\//-}"
|
||||
echo "ARTIFACT_SUFFIX=${BRANCH_NAME}-${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
@@ -134,8 +171,11 @@ jobs:
|
||||
env:
|
||||
SAFE_PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
SAFE_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
ARTIFACT_SUFFIX: ${{ env.ARTIFACT_SUFFIX }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
PR_INFO="**PR:** #${SAFE_PR_NUMBER} — ${SAFE_PR_TITLE}"
|
||||
else
|
||||
PR_INFO=""
|
||||
@@ -144,14 +184,14 @@ jobs:
|
||||
cat >> "${GITHUB_STEP_SUMMARY}" <<SUMMARY
|
||||
## Android APK Build Complete
|
||||
|
||||
**Commit:** ${{ github.sha }}
|
||||
**Commit:** ${COMMIT_SHA}
|
||||
${PR_INFO}
|
||||
|
||||
### Download APKs
|
||||
1. Go to the **Actions** tab
|
||||
2. Click on this workflow run
|
||||
3. Scroll down to **Artifacts**
|
||||
4. Download **apk-staging-${{ env.ARTIFACT_SUFFIX }}**
|
||||
4. Download **apk-staging-${ARTIFACT_SUFFIX}**
|
||||
|
||||
### APK Files
|
||||
SUMMARY
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.daemon=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
+57
-54
@@ -1,25 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Android targets
|
||||
set -e # Exit on any error
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🚀 Building Rust library for Android targets..."
|
||||
|
||||
# Function to print colored output
|
||||
print_step() {
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "\033[1;33m⚠️ $1\033[0m"
|
||||
echo -e "\033[1;33m⚠️ $1\033[0m"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
}
|
||||
|
||||
# Auto-detect Android SDK and NDK paths
|
||||
@@ -27,59 +27,62 @@ print_step "Detecting Android SDK and NDK paths"
|
||||
|
||||
# Try to find Android SDK
|
||||
if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then
|
||||
# Common Android SDK locations
|
||||
POSSIBLE_SDK_PATHS=(
|
||||
"$HOME/Library/Android/sdk" # macOS default
|
||||
"$HOME/Android/Sdk" # Linux default
|
||||
"$HOME/AppData/Local/Android/Sdk" # Windows default
|
||||
"/opt/android-sdk" # Linux alternative
|
||||
)
|
||||
# Common Android SDK locations
|
||||
POSSIBLE_SDK_PATHS=(
|
||||
"$HOME/Library/Android/sdk" # macOS default
|
||||
"$HOME/Android/Sdk" # Linux default
|
||||
"$HOME/AppData/Local/Android/Sdk" # Windows default
|
||||
"/opt/android-sdk" # Linux alternative
|
||||
)
|
||||
|
||||
for path in "${POSSIBLE_SDK_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
export ANDROID_HOME="$path"
|
||||
export ANDROID_SDK_ROOT="$path"
|
||||
print_success "Found Android SDK at: $path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
for path in "${POSSIBLE_SDK_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
export ANDROID_HOME="$path"
|
||||
export ANDROID_SDK_ROOT="$path"
|
||||
print_success "Found Android SDK at: $path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
else
|
||||
export ANDROID_HOME="${ANDROID_HOME:-$ANDROID_SDK_ROOT}"
|
||||
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
|
||||
print_success "Using Android SDK from environment: $ANDROID_HOME"
|
||||
export ANDROID_HOME="${ANDROID_HOME:-$ANDROID_SDK_ROOT}"
|
||||
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
|
||||
print_success "Using Android SDK from environment: $ANDROID_HOME"
|
||||
fi
|
||||
|
||||
if [ -z "$ANDROID_HOME" ]; then
|
||||
print_error "Android SDK not found. Please install Android SDK or set ANDROID_HOME environment variable"
|
||||
exit 1
|
||||
print_error "Android SDK not found. Please install Android SDK or set ANDROID_HOME environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-detect NDK path
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
# Look for NDK in the SDK directory
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
# Find the latest NDK version
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
export ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
print_success "Found NDK version $NDK_VERSION at: $ANDROID_NDK_HOME"
|
||||
fi
|
||||
# Look for NDK in the SDK directory
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
# Find the latest NDK version
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
export ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
print_success "Found NDK version $NDK_VERSION at: $ANDROID_NDK_HOME"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then
|
||||
print_error "Android NDK not found. Please install Android NDK or set ANDROID_NDK_HOME environment variable"
|
||||
print_error "You can install NDK through Android Studio SDK Manager"
|
||||
exit 1
|
||||
print_error "Android NDK not found. Please install Android NDK or set ANDROID_NDK_HOME environment variable"
|
||||
print_error "You can install NDK through Android Studio SDK Manager"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect host OS for NDK toolchain path
|
||||
case "$(uname -s)" in
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
MINGW*|CYGWIN*|MSYS*) HOST_TAG="windows-x86_64" ;;
|
||||
*) print_error "Unsupported host OS: $(uname -s)"; exit 1 ;;
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
MINGW* | CYGWIN* | MSYS*) HOST_TAG="windows-x86_64" ;;
|
||||
*)
|
||||
print_error "Unsupported host OS: $(uname -s)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG/bin:$PATH"
|
||||
@@ -96,9 +99,9 @@ OPENSSL_ANDROID_DIR="$PWD/rust/target/openssl/install"
|
||||
|
||||
# Check if required tools are installed
|
||||
print_step "Checking development environment"
|
||||
if ! command -v rustup &> /dev/null; then
|
||||
print_error "Rustup is not installed or not in PATH"
|
||||
exit 1
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
print_error "Rustup is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate Cargo config for current environment
|
||||
@@ -107,9 +110,9 @@ print_step "Setting up Cargo configuration"
|
||||
|
||||
# Add Android targets
|
||||
print_step "Adding Android targets to Rust"
|
||||
rustup target add aarch64-linux-android # arm64-v8a (64-bit ARM)
|
||||
rustup target add armv7-linux-androideabi # armeabi-v7a (32-bit ARM)
|
||||
rustup target add x86_64-linux-android # x86_64 (64-bit Intel)
|
||||
rustup target add aarch64-linux-android # arm64-v8a (64-bit ARM)
|
||||
rustup target add armv7-linux-androideabi # armeabi-v7a (32-bit ARM)
|
||||
rustup target add x86_64-linux-android # x86_64 (64-bit Intel)
|
||||
print_success "Android targets added to Rust"
|
||||
|
||||
# Create output directories
|
||||
@@ -124,19 +127,19 @@ cd rust
|
||||
|
||||
# Build helper: sets per-target OpenSSL env vars and runs cargo build
|
||||
build_for_target() {
|
||||
local target="$1"
|
||||
local label="$2"
|
||||
local jni_dir="$3"
|
||||
local target="$1"
|
||||
local label="$2"
|
||||
local jni_dir="$3"
|
||||
|
||||
print_step "Building for $label"
|
||||
OPENSSL_DIR="$OPENSSL_ANDROID_DIR/$target" \
|
||||
print_step "Building for $label"
|
||||
OPENSSL_DIR="$OPENSSL_ANDROID_DIR/$target" \
|
||||
OPENSSL_INCLUDE_DIR="$OPENSSL_ANDROID_DIR/$target/include" \
|
||||
OPENSSL_LIB_DIR="$OPENSSL_ANDROID_DIR/$target/lib" \
|
||||
OPENSSL_STATIC=1 \
|
||||
OPENSSL_NO_VENDOR=1 \
|
||||
cargo build --target "$target" --release --quiet
|
||||
cp "target/$target/release/librust_lib_whitenoise.so" "../android/app/src/main/jniLibs/$jni_dir/"
|
||||
print_success "Built for $label"
|
||||
cp "target/$target/release/librust_lib_whitenoise.so" "../android/app/src/main/jniLibs/$jni_dir/"
|
||||
print_success "Built for $label"
|
||||
}
|
||||
|
||||
build_for_target "aarch64-linux-android" "aarch64 (arm64-v8a)" "arm64-v8a"
|
||||
|
||||
+28
-25
@@ -1,56 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for iOS targets
|
||||
set -e # Exit on any error
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🚀 Building Rust library for iOS targets..."
|
||||
|
||||
# Function to print colored output
|
||||
print_step() {
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
}
|
||||
|
||||
# Check if required tools are installed
|
||||
print_step "Checking development environment"
|
||||
if ! command -v rustup &> /dev/null; then
|
||||
print_error "Rustup is not installed or not in PATH"
|
||||
exit 1
|
||||
if ! command -v rustup &>/dev/null; then
|
||||
print_error "Rustup is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
print_error "Cargo is not installed or not in PATH"
|
||||
exit 1
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
print_error "Cargo is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcodebuild &> /dev/null; then
|
||||
print_error "Xcode command line tools are not installed"
|
||||
exit 1
|
||||
if ! command -v xcodebuild &>/dev/null; then
|
||||
print_error "Xcode command line tools are not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v pod &> /dev/null; then
|
||||
print_error "CocoaPods is not installed or not in PATH"
|
||||
exit 1
|
||||
if ! command -v pod &>/dev/null; then
|
||||
print_error "CocoaPods is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add iOS targets
|
||||
print_step "Adding iOS targets to Rust"
|
||||
rustup target add aarch64-apple-ios # Physical devices (arm64)
|
||||
rustup target add aarch64-apple-ios-sim # Simulator on Apple Silicon
|
||||
rustup target add aarch64-apple-ios # Physical devices (arm64)
|
||||
rustup target add aarch64-apple-ios-sim # Simulator on Apple Silicon
|
||||
print_success "iOS targets added to Rust"
|
||||
|
||||
# Build for each iOS architecture
|
||||
print_step "Building for iOS architectures"
|
||||
if ! test -d "rust"; then
|
||||
print_error "rust directory not found"
|
||||
exit 1
|
||||
print_error "rust directory not found"
|
||||
exit 1
|
||||
fi
|
||||
cd rust
|
||||
|
||||
@@ -66,13 +66,16 @@ cd ..
|
||||
|
||||
# Run pod install to ensure pods are up to date
|
||||
print_step "Installing CocoaPods dependencies"
|
||||
pushd ios > /dev/null || { print_error "Failed to enter ios directory"; exit 1; }
|
||||
pushd ios >/dev/null || {
|
||||
print_error "Failed to enter ios directory"
|
||||
exit 1
|
||||
}
|
||||
if ! pod install --silent; then
|
||||
print_error "pod install failed"
|
||||
popd > /dev/null
|
||||
exit 1
|
||||
print_error "pod install failed"
|
||||
popd >/dev/null
|
||||
exit 1
|
||||
fi
|
||||
popd > /dev/null
|
||||
popd >/dev/null
|
||||
|
||||
print_success "All Rust libraries built for iOS"
|
||||
print_success "iOS build completed successfully!"
|
||||
|
||||
+123
-120
@@ -6,47 +6,47 @@
|
||||
set -eo pipefail
|
||||
|
||||
print_step() {
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
echo -e "\n\033[1;34m=== $1 ===\033[0m"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "\033[1;32m$1\033[0m"
|
||||
echo -e "\033[1;32m$1\033[0m"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\033[1;31m$1\033[0m"
|
||||
echo -e "\033[1;31m$1\033[0m"
|
||||
}
|
||||
|
||||
# Spinner for long-running commands
|
||||
# Usage: run_quiet "description" logfile command [args...]
|
||||
run_quiet() {
|
||||
local desc="$1"
|
||||
local logfile="$2"
|
||||
shift 2
|
||||
local desc="$1"
|
||||
local logfile="$2"
|
||||
shift 2
|
||||
|
||||
printf " %-40s " "$desc"
|
||||
printf " %-40s " "$desc"
|
||||
|
||||
# Run command in background with output redirected to log file
|
||||
"$@" >> "$logfile" 2>&1 &
|
||||
local pid=$!
|
||||
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||
local i=0
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
printf "\b%s" "${spin:i++%${#spin}:1}"
|
||||
sleep 0.1
|
||||
done
|
||||
wait "$pid"
|
||||
local exit_code=$?
|
||||
# Run command in background with output redirected to log file
|
||||
"$@" >>"$logfile" 2>&1 &
|
||||
local pid=$!
|
||||
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||
local i=0
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
printf "\b%s" "${spin:i++%${#spin}:1}"
|
||||
sleep 0.1
|
||||
done
|
||||
wait "$pid"
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
printf "\b\033[1;32m✓\033[0m\n"
|
||||
else
|
||||
printf "\b\033[1;31m✗\033[0m\n"
|
||||
print_error "Command failed. Last 20 lines of log:"
|
||||
tail -20 "$logfile"
|
||||
print_error "Full log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
printf "\b\033[1;32m✓\033[0m\n"
|
||||
else
|
||||
printf "\b\033[1;31m✗\033[0m\n"
|
||||
print_error "Command failed. Last 20 lines of log:"
|
||||
tail -20 "$logfile"
|
||||
print_error "Full log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Configuration
|
||||
@@ -63,136 +63,139 @@ OPENSSL_INSTALL_DIR="$OPENSSL_BUILD_DIR/install"
|
||||
|
||||
# Auto-detect Android NDK
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
if [ -n "$NDK_HOME" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_HOME"
|
||||
elif [ -n "$ANDROID_HOME" ]; then
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$NDK_HOME" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_HOME"
|
||||
elif [ -n "$ANDROID_HOME" ]; then
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then
|
||||
print_error "Android NDK not found. Set ANDROID_NDK_HOME or NDK_HOME."
|
||||
exit 1
|
||||
print_error "Android NDK not found. Set ANDROID_NDK_HOME or NDK_HOME."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect host OS
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
case "$(uname -m)" in
|
||||
arm64|aarch64) HOST_TAG="darwin-arm64" ;;
|
||||
*) HOST_TAG="darwin-x86_64" ;;
|
||||
esac
|
||||
;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
*) print_error "Unsupported host OS: $(uname -s)"; exit 1 ;;
|
||||
Darwin*)
|
||||
case "$(uname -m)" in
|
||||
arm64 | aarch64) HOST_TAG="darwin-arm64" ;;
|
||||
*) HOST_TAG="darwin-x86_64" ;;
|
||||
esac
|
||||
;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
*)
|
||||
print_error "Unsupported host OS: $(uname -s)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
TOOLCHAIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG"
|
||||
|
||||
# Target configurations: (rust_target openssl_target arch)
|
||||
TARGETS=(
|
||||
"aarch64-linux-android:android-arm64:aarch64"
|
||||
"armv7-linux-androideabi:android-arm:armv7a"
|
||||
"x86_64-linux-android:android-x86_64:x86_64"
|
||||
"aarch64-linux-android:android-arm64:aarch64"
|
||||
"armv7-linux-androideabi:android-arm:armv7a"
|
||||
"x86_64-linux-android:android-x86_64:x86_64"
|
||||
)
|
||||
|
||||
build_openssl_for_target() {
|
||||
local rust_target="$1"
|
||||
local openssl_target="$2"
|
||||
local arch="$3"
|
||||
local install_prefix="$OPENSSL_INSTALL_DIR/$rust_target"
|
||||
local rust_target="$1"
|
||||
local openssl_target="$2"
|
||||
local arch="$3"
|
||||
local install_prefix="$OPENSSL_INSTALL_DIR/$rust_target"
|
||||
|
||||
# Skip if already built
|
||||
if [ -f "$install_prefix/lib/libcrypto.a" ]; then
|
||||
print_success "OpenSSL already built for $rust_target (skipping)"
|
||||
return 0
|
||||
fi
|
||||
# Skip if already built
|
||||
if [ -f "$install_prefix/lib/libcrypto.a" ]; then
|
||||
print_success "OpenSSL already built for $rust_target (skipping)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_step "Building OpenSSL for $rust_target ($openssl_target)"
|
||||
print_step "Building OpenSSL for $rust_target ($openssl_target)"
|
||||
|
||||
# Clean and re-extract source for each target (OpenSSL doesn't support out-of-tree builds well)
|
||||
local build_src="$OPENSSL_BUILD_DIR/build-$rust_target"
|
||||
local logfile="$OPENSSL_BUILD_DIR/$rust_target.log"
|
||||
rm -rf "$build_src"
|
||||
: > "$logfile"
|
||||
cp -r "$OPENSSL_SRC_DIR" "$build_src"
|
||||
cd "$build_src"
|
||||
# Clean and re-extract source for each target (OpenSSL doesn't support out-of-tree builds well)
|
||||
local build_src="$OPENSSL_BUILD_DIR/build-$rust_target"
|
||||
local logfile="$OPENSSL_BUILD_DIR/$rust_target.log"
|
||||
rm -rf "$build_src"
|
||||
: >"$logfile"
|
||||
cp -r "$OPENSSL_SRC_DIR" "$build_src"
|
||||
cd "$build_src"
|
||||
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
export PATH="$TOOLCHAIN/bin:$PATH"
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
export PATH="$TOOLCHAIN/bin:$PATH"
|
||||
|
||||
run_quiet "Configuring ($openssl_target)..." "$logfile" \
|
||||
./Configure "$openssl_target" \
|
||||
-D__ANDROID_API__=$ANDROID_API \
|
||||
--prefix="$install_prefix" \
|
||||
--openssldir="$install_prefix/ssl" \
|
||||
no-shared \
|
||||
no-tests \
|
||||
no-ui-console \
|
||||
no-stdio \
|
||||
-fPIC
|
||||
run_quiet "Configuring ($openssl_target)..." "$logfile" \
|
||||
./Configure "$openssl_target" \
|
||||
-D__ANDROID_API__=$ANDROID_API \
|
||||
--prefix="$install_prefix" \
|
||||
--openssldir="$install_prefix/ssl" \
|
||||
no-shared \
|
||||
no-tests \
|
||||
no-ui-console \
|
||||
no-stdio \
|
||||
-fPIC
|
||||
|
||||
run_quiet "Compiling..." "$logfile" \
|
||||
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" build_libs
|
||||
run_quiet "Compiling..." "$logfile" \
|
||||
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" build_libs
|
||||
|
||||
run_quiet "Installing headers & libs..." "$logfile" \
|
||||
make install_dev
|
||||
run_quiet "Installing headers & libs..." "$logfile" \
|
||||
make install_dev
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
rm -rf "$build_src"
|
||||
cd "$PROJECT_ROOT"
|
||||
rm -rf "$build_src"
|
||||
|
||||
if [ -f "$install_prefix/lib/libcrypto.a" ]; then
|
||||
print_success " Built OpenSSL for $rust_target"
|
||||
else
|
||||
print_error "Failed to build OpenSSL for $rust_target"
|
||||
print_error "Check log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$install_prefix/lib/libcrypto.a" ]; then
|
||||
print_success " Built OpenSSL for $rust_target"
|
||||
else
|
||||
print_error "Failed to build OpenSSL for $rust_target"
|
||||
print_error "Check log: $logfile"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Download OpenSSL source if needed
|
||||
if [ ! -d "$OPENSSL_SRC_DIR" ]; then
|
||||
print_step "Downloading OpenSSL $OPENSSL_VERSION"
|
||||
mkdir -p "$OPENSSL_BUILD_DIR"
|
||||
cd "$OPENSSL_BUILD_DIR"
|
||||
print_step "Downloading OpenSSL $OPENSSL_VERSION"
|
||||
mkdir -p "$OPENSSL_BUILD_DIR"
|
||||
cd "$OPENSSL_BUILD_DIR"
|
||||
|
||||
TARBALL="openssl-$OPENSSL_VERSION.tar.gz"
|
||||
if [ ! -f "$TARBALL" ]; then
|
||||
curl -L --progress-bar -o "$TARBALL" "https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/$TARBALL"
|
||||
fi
|
||||
TARBALL="openssl-$OPENSSL_VERSION.tar.gz"
|
||||
if [ ! -f "$TARBALL" ]; then
|
||||
curl -L --progress-bar -o "$TARBALL" "https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/$TARBALL"
|
||||
fi
|
||||
|
||||
print_step "Verifying tarball integrity"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
ACTUAL_SHA256=$(shasum -a 256 "$TARBALL" | awk '{print $1}')
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
ACTUAL_SHA256=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||
else
|
||||
print_error "No SHA-256 tool found (need shasum or sha256sum)"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_SHA256" != "$OPENSSL_SHA256" ]; then
|
||||
print_error "SHA-256 mismatch for $TARBALL"
|
||||
print_error " Expected: $OPENSSL_SHA256"
|
||||
print_error " Actual: $ACTUAL_SHA256"
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
fi
|
||||
print_success "SHA-256 verified"
|
||||
print_step "Verifying tarball integrity"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
ACTUAL_SHA256=$(shasum -a 256 "$TARBALL" | awk '{print $1}')
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
ACTUAL_SHA256=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||
else
|
||||
print_error "No SHA-256 tool found (need shasum or sha256sum)"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ACTUAL_SHA256" != "$OPENSSL_SHA256" ]; then
|
||||
print_error "SHA-256 mismatch for $TARBALL"
|
||||
print_error " Expected: $OPENSSL_SHA256"
|
||||
print_error " Actual: $ACTUAL_SHA256"
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
fi
|
||||
print_success "SHA-256 verified"
|
||||
|
||||
tar xzf "$TARBALL"
|
||||
cd "$PROJECT_ROOT"
|
||||
tar xzf "$TARBALL"
|
||||
cd "$PROJECT_ROOT"
|
||||
fi
|
||||
|
||||
# Build for each target
|
||||
for target_spec in "${TARGETS[@]}"; do
|
||||
IFS=':' read -r rust_target openssl_target arch <<< "$target_spec"
|
||||
build_openssl_for_target "$rust_target" "$openssl_target" "$arch"
|
||||
IFS=':' read -r rust_target openssl_target arch <<<"$target_spec"
|
||||
build_openssl_for_target "$rust_target" "$openssl_target" "$arch"
|
||||
done
|
||||
|
||||
print_success "All OpenSSL Android builds complete!"
|
||||
|
||||
+92
-83
@@ -24,11 +24,11 @@ BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_step() { echo -e "\n${BLUE}=== $1 ===${NC}"; }
|
||||
print_step() { echo -e "\n${BLUE}=== $1 ===${NC}"; }
|
||||
print_success() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||
print_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||
print_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||
print_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
@@ -39,31 +39,40 @@ BUILD_IOS=false
|
||||
CUSTOM_OUTPUT_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--android) BUILD_ANDROID=true; shift ;;
|
||||
--ios) BUILD_IOS=true; shift ;;
|
||||
--output-dir) CUSTOM_OUTPUT_DIR="$2"; shift 2 ;;
|
||||
--help)
|
||||
sed -n '/^# Usage:/,/^[^#]/p' "$0" | sed 's/^# \{0,2\}//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
case $1 in
|
||||
--android)
|
||||
BUILD_ANDROID=true
|
||||
shift
|
||||
;;
|
||||
--ios)
|
||||
BUILD_IOS=true
|
||||
shift
|
||||
;;
|
||||
--output-dir)
|
||||
CUSTOM_OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
sed -n '/^# Usage:/,/^[^#]/p' "$0" | sed 's/^# \{0,2\}//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default: build both (iOS only on macOS)
|
||||
if [ "$BUILD_ANDROID" = false ] && [ "$BUILD_IOS" = false ]; then
|
||||
BUILD_ANDROID=true
|
||||
[[ "$OSTYPE" == "darwin"* ]] && BUILD_IOS=true
|
||||
BUILD_ANDROID=true
|
||||
[[ "$OSTYPE" == "darwin"* ]] && BUILD_IOS=true
|
||||
fi
|
||||
|
||||
# iOS requires macOS
|
||||
if [ "$BUILD_IOS" = true ] && [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
print_warning "iOS builds require macOS — skipping iOS."
|
||||
BUILD_IOS=false
|
||||
print_warning "iOS builds require macOS — skipping iOS."
|
||||
BUILD_IOS=false
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,8 +81,8 @@ fi
|
||||
|
||||
VERSION_LINE=$(grep "^version:" pubspec.yaml)
|
||||
if [ -z "$VERSION_LINE" ]; then
|
||||
print_error "Could not find version in pubspec.yaml"
|
||||
exit 1
|
||||
print_error "Could not find version in pubspec.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FULL_VERSION=$(echo "$VERSION_LINE" | sed 's/version: //' | tr -d ' ')
|
||||
@@ -85,9 +94,9 @@ BUILD_NUMBER=$(echo "$FULL_VERSION" | cut -d'+' -f2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ -n "$CUSTOM_OUTPUT_DIR" ]; then
|
||||
OUTPUT_DIR="$CUSTOM_OUTPUT_DIR"
|
||||
OUTPUT_DIR="$CUSTOM_OUTPUT_DIR"
|
||||
else
|
||||
OUTPUT_DIR="build/releases/v${VERSION_NAME}+${BUILD_NUMBER}"
|
||||
OUTPUT_DIR="build/releases/v${VERSION_NAME}+${BUILD_NUMBER}"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -106,43 +115,43 @@ mkdir -p "$OUTPUT_DIR"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ "$BUILD_ANDROID" = true ]; then
|
||||
print_step "Building Android"
|
||||
print_info "Building Rust native libraries..."
|
||||
./scripts/build_android.sh
|
||||
print_step "Building Android"
|
||||
print_info "Building Rust native libraries..."
|
||||
./scripts/build_android.sh
|
||||
|
||||
print_info "Building split APKs (production flavor)..."
|
||||
flutter build apk --flavor production --split-per-abi
|
||||
print_info "Building split APKs (production flavor)..."
|
||||
flutter build apk --flavor production --split-per-abi
|
||||
|
||||
APK_DIR="build/app/outputs/flutter-apk"
|
||||
APK_DIR="build/app/outputs/flutter-apk"
|
||||
|
||||
print_step "Staging Android artifacts"
|
||||
print_step "Staging Android artifacts"
|
||||
|
||||
for ABI in arm64-v8a armeabi-v7a x86_64; do
|
||||
# Flutter names the file: app-<abi>-production-release.apk
|
||||
SRC=$(find "$APK_DIR" -name "app-${ABI}-production-release.apk" -type f | head -n 1)
|
||||
for ABI in arm64-v8a armeabi-v7a x86_64; do
|
||||
# Flutter names the file: app-<abi>-production-release.apk
|
||||
SRC=$(find "$APK_DIR" -name "app-${ABI}-production-release.apk" -type f | head -n 1)
|
||||
|
||||
if [ -z "$SRC" ] || [ ! -f "$SRC" ]; then
|
||||
if [ "$ABI" = "armeabi-v7a" ]; then
|
||||
print_warning "armeabi-v7a APK not found (expected for modern-only builds)"
|
||||
else
|
||||
print_error "$ABI APK not found in $APK_DIR"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ -z "$SRC" ] || [ ! -f "$SRC" ]; then
|
||||
if [ "$ABI" = "armeabi-v7a" ]; then
|
||||
print_warning "armeabi-v7a APK not found (expected for modern-only builds)"
|
||||
else
|
||||
print_error "$ABI APK not found in $APK_DIR"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
DEST_NAME="whitenoise-${VERSION_NAME}-${ABI}.apk"
|
||||
DEST="$OUTPUT_DIR/$DEST_NAME"
|
||||
DEST_NAME="whitenoise-${VERSION_NAME}-${ABI}.apk"
|
||||
DEST="$OUTPUT_DIR/$DEST_NAME"
|
||||
|
||||
cp "$SRC" "$DEST"
|
||||
cp "$SRC" "$DEST"
|
||||
|
||||
# SHA-256 file hash (download integrity)
|
||||
HASH=$(shasum -a 256 "$DEST" | awk '{print $1}')
|
||||
echo "$HASH $DEST_NAME" > "${DEST}.sha256"
|
||||
# SHA-256 file hash (download integrity)
|
||||
HASH=$(shasum -a 256 "$DEST" | awk '{print $1}')
|
||||
echo "$HASH $DEST_NAME" >"${DEST}.sha256"
|
||||
|
||||
print_success "$DEST_NAME"
|
||||
print_info " SHA-256: $HASH"
|
||||
print_info " Hash file: ${DEST_NAME}.sha256"
|
||||
done
|
||||
print_success "$DEST_NAME"
|
||||
print_info " SHA-256: $HASH"
|
||||
print_info " Hash file: ${DEST_NAME}.sha256"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -150,22 +159,22 @@ fi
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ "$BUILD_IOS" = true ]; then
|
||||
print_step "Building iOS"
|
||||
print_info "Building Rust native libraries..."
|
||||
./scripts/build_ios.sh
|
||||
print_step "Building iOS"
|
||||
print_info "Building Rust native libraries..."
|
||||
./scripts/build_ios.sh
|
||||
|
||||
print_info "Building IPA (production flavor)..."
|
||||
flutter build ipa --flavor production --export-method app-store
|
||||
print_info "Building IPA (production flavor)..."
|
||||
flutter build ipa --flavor production --export-method app-store
|
||||
|
||||
IPA_PATH=$(find build/ios -name "*.ipa" -type f | head -n 1)
|
||||
IPA_PATH=$(find build/ios -name "*.ipa" -type f | head -n 1)
|
||||
|
||||
if [ -n "$IPA_PATH" ] && [ -f "$IPA_PATH" ]; then
|
||||
DEST_NAME="whitenoise-${VERSION_NAME}+${BUILD_NUMBER}.ipa"
|
||||
cp "$IPA_PATH" "$OUTPUT_DIR/$DEST_NAME"
|
||||
print_success "$DEST_NAME"
|
||||
else
|
||||
print_warning "IPA not found after build — check signing configuration"
|
||||
fi
|
||||
if [ -n "$IPA_PATH" ] && [ -f "$IPA_PATH" ]; then
|
||||
DEST_NAME="whitenoise-${VERSION_NAME}+${BUILD_NUMBER}.ipa"
|
||||
cp "$IPA_PATH" "$OUTPUT_DIR/$DEST_NAME"
|
||||
print_success "$DEST_NAME"
|
||||
else
|
||||
print_warning "IPA not found after build — check signing configuration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -175,24 +184,24 @@ fi
|
||||
print_step "Writing build_info.txt"
|
||||
|
||||
{
|
||||
echo "White Noise Build Information"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "Version: $VERSION_NAME"
|
||||
echo "Build Number: $BUILD_NUMBER"
|
||||
echo "Build Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "Git Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
|
||||
echo "Git Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"
|
||||
echo ""
|
||||
echo "Artifacts:"
|
||||
for f in "$OUTPUT_DIR"/*.apk "$OUTPUT_DIR"/*.ipa; do
|
||||
[ -f "$f" ] || continue
|
||||
fname=$(basename "$f")
|
||||
hash=$(shasum -a 256 "$f" | awk '{print $1}')
|
||||
echo " $fname"
|
||||
echo " SHA-256: $hash"
|
||||
done
|
||||
} > "$OUTPUT_DIR/build_info.txt"
|
||||
echo "White Noise Build Information"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "Version: $VERSION_NAME"
|
||||
echo "Build Number: $BUILD_NUMBER"
|
||||
echo "Build Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "Git Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
|
||||
echo "Git Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"
|
||||
echo ""
|
||||
echo "Artifacts:"
|
||||
for f in "$OUTPUT_DIR"/*.apk "$OUTPUT_DIR"/*.ipa; do
|
||||
[ -f "$f" ] || continue
|
||||
fname=$(basename "$f")
|
||||
hash=$(shasum -a 256 "$f" | awk '{print $1}')
|
||||
echo " $fname"
|
||||
echo " SHA-256: $hash"
|
||||
done
|
||||
} >"$OUTPUT_DIR/build_info.txt"
|
||||
|
||||
print_success "build_info.txt written"
|
||||
|
||||
|
||||
+115
-115
@@ -19,141 +19,141 @@ MIN_COVERAGE=""
|
||||
WARNINGS_FILE=""
|
||||
|
||||
print_error() {
|
||||
printf '%b%s%b\n' "\e[31;1m" "$1" "\e[0m" >&2
|
||||
printf '%b%s%b\n' "\e[31;1m" "$1" "\e[0m" >&2
|
||||
}
|
||||
|
||||
print_success() {
|
||||
printf '%b%s%b\n' "\e[32;1m" "$1" "\e[0m" >&2
|
||||
printf '%b%s%b\n' "\e[32;1m" "$1" "\e[0m" >&2
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
printf '%b%s%b\n' "\e[33;1m" "$1" "\e[0m" >&2
|
||||
printf '%b%s%b\n' "\e[33;1m" "$1" "\e[0m" >&2
|
||||
}
|
||||
|
||||
raise_error() {
|
||||
print_error "$1"
|
||||
exit 1
|
||||
print_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
parse_arguments() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--min)
|
||||
MIN_COVERAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--warnings-file)
|
||||
WARNINGS_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
LCOV_FILE="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--min)
|
||||
MIN_COVERAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--warnings-file)
|
||||
WARNINGS_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
LCOV_FILE="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
check_lcov_file_presence() {
|
||||
if [ ! -f "$LCOV_FILE" ]; then
|
||||
raise_error "Error: $LCOV_FILE not found. Run 'flutter test --coverage' first."
|
||||
fi
|
||||
if [ ! -f "$LCOV_FILE" ]; then
|
||||
raise_error "Error: $LCOV_FILE not found. Run 'flutter test --coverage' first."
|
||||
fi
|
||||
}
|
||||
|
||||
is_generated_file() {
|
||||
local file="$1"
|
||||
if [[ "$file" == *".freezed.dart" ]] ||
|
||||
[[ "$file" == *".g.dart" ]] ||
|
||||
[[ "$file" == lib/src/rust/* ]] ||
|
||||
[[ "$file" == lib/l10n/generated/* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
local file="$1"
|
||||
if [[ "$file" == *".freezed.dart" ]] ||
|
||||
[[ "$file" == *".g.dart" ]] ||
|
||||
[[ "$file" == lib/src/rust/* ]] ||
|
||||
[[ "$file" == lib/l10n/generated/* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
is_coverage_ignored() {
|
||||
local file="$1"
|
||||
grep -q '// coverage:ignore-file' "$file"
|
||||
local file="$1"
|
||||
grep -q '// coverage:ignore-file' "$file"
|
||||
}
|
||||
|
||||
count_lines() {
|
||||
local file="$1"
|
||||
wc -l <"$file" | tr -d ' '
|
||||
local file="$1"
|
||||
wc -l <"$file" | tr -d ' '
|
||||
}
|
||||
|
||||
get_covered_files() {
|
||||
grep "^SF:" "$LCOV_FILE" | sed 's/^SF://' | sort -u
|
||||
grep "^SF:" "$LCOV_FILE" | sed 's/^SF://' | sort -u
|
||||
}
|
||||
|
||||
generate_zero_coverage_record() {
|
||||
local file="$1"
|
||||
local line_count="$2"
|
||||
local file="$1"
|
||||
local line_count="$2"
|
||||
|
||||
echo "SF:$file"
|
||||
for ((i = 1; i <= line_count; i++)); do
|
||||
echo "DA:$i,0"
|
||||
done
|
||||
echo "LF:$line_count"
|
||||
echo "LH:0"
|
||||
echo "end_of_record"
|
||||
echo "SF:$file"
|
||||
for ((i = 1; i <= line_count; i++)); do
|
||||
echo "DA:$i,0"
|
||||
done
|
||||
echo "LF:$line_count"
|
||||
echo "LH:0"
|
||||
echo "end_of_record"
|
||||
}
|
||||
|
||||
print_missing_files_warning() {
|
||||
local missing_count="$1"
|
||||
shift
|
||||
local missing_files=("$@")
|
||||
local missing_count="$1"
|
||||
shift
|
||||
local missing_files=("$@")
|
||||
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
print_warning "⚠️ Found $missing_count file(s) with no test coverage:"
|
||||
for file in "${missing_files[@]}"; do
|
||||
print_warning " - $file"
|
||||
done
|
||||
fi
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
print_warning "⚠️ Found $missing_count file(s) with no test coverage:"
|
||||
for file in "${missing_files[@]}"; do
|
||||
print_warning " - $file"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
inject_missing_files() {
|
||||
local missing_count=0
|
||||
local missing_files=()
|
||||
local covered_files
|
||||
covered_files=$(get_covered_files)
|
||||
local missing_count=0
|
||||
local missing_files=()
|
||||
local covered_files
|
||||
covered_files=$(get_covered_files)
|
||||
|
||||
while IFS= read -r -d '' dart_file; do
|
||||
dart_file="${dart_file#./}"
|
||||
while IFS= read -r -d '' dart_file; do
|
||||
dart_file="${dart_file#./}"
|
||||
|
||||
if is_generated_file "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
if is_generated_file "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if is_coverage_ignored "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
if is_coverage_ignored "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if echo "$covered_files" | grep -qx "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
if echo "$covered_files" | grep -qx "$dart_file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local line_count
|
||||
line_count=$(count_lines "$dart_file")
|
||||
local line_count
|
||||
line_count=$(count_lines "$dart_file")
|
||||
|
||||
if [ "$line_count" -gt 0 ]; then
|
||||
generate_zero_coverage_record "$dart_file" "$line_count" >>"$LCOV_FILE"
|
||||
missing_files+=("$dart_file")
|
||||
missing_count=$((missing_count + 1))
|
||||
fi
|
||||
done < <(find lib -name "*.dart" -type f -print0 2>/dev/null)
|
||||
if [ "$line_count" -gt 0 ]; then
|
||||
generate_zero_coverage_record "$dart_file" "$line_count" >>"$LCOV_FILE"
|
||||
missing_files+=("$dart_file")
|
||||
missing_count=$((missing_count + 1))
|
||||
fi
|
||||
done < <(find lib -name "*.dart" -type f -print0 2>/dev/null)
|
||||
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
print_missing_files_warning "$missing_count" "${missing_files[@]}"
|
||||
if [ -n "$WARNINGS_FILE" ]; then
|
||||
printf '%s\n' "${missing_files[@]}" >"$WARNINGS_FILE"
|
||||
fi
|
||||
elif [ -n "$WARNINGS_FILE" ]; then
|
||||
: >"$WARNINGS_FILE"
|
||||
fi
|
||||
if [ "$missing_count" -gt 0 ]; then
|
||||
print_missing_files_warning "$missing_count" "${missing_files[@]}"
|
||||
if [ -n "$WARNINGS_FILE" ]; then
|
||||
printf '%s\n' "${missing_files[@]}" >"$WARNINGS_FILE"
|
||||
fi
|
||||
elif [ -n "$WARNINGS_FILE" ]; then
|
||||
: >"$WARNINGS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
filter_generated_files() {
|
||||
awk '
|
||||
awk '
|
||||
BEGIN { skip = 0 }
|
||||
/^SF:/ {
|
||||
file = substr($0, 4)
|
||||
@@ -172,11 +172,11 @@ filter_generated_files() {
|
||||
}
|
||||
{ if (!skip) print }
|
||||
' "$LCOV_FILE" >"${LCOV_FILE}.tmp"
|
||||
mv "${LCOV_FILE}.tmp" "$LCOV_FILE"
|
||||
mv "${LCOV_FILE}.tmp" "$LCOV_FILE"
|
||||
}
|
||||
|
||||
calculate_coverage() {
|
||||
awk '
|
||||
awk '
|
||||
BEGIN {
|
||||
total_lines = 0
|
||||
covered_lines = 0
|
||||
@@ -209,43 +209,43 @@ calculate_coverage() {
|
||||
}
|
||||
|
||||
check_coverage_threshold() {
|
||||
local coverage="$1"
|
||||
local min_coverage="$2"
|
||||
local coverage="$1"
|
||||
local min_coverage="$2"
|
||||
|
||||
if (($(awk "BEGIN {print ($coverage >= $min_coverage)}"))); then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
if (($(awk "BEGIN {print ($coverage >= $min_coverage)}"))); then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
print_coverage_result() {
|
||||
local coverage="$1"
|
||||
local min_coverage="$2"
|
||||
local coverage="$1"
|
||||
local min_coverage="$2"
|
||||
|
||||
if check_coverage_threshold "$coverage" "$min_coverage"; then
|
||||
print_success "✅ Coverage: ${coverage}%"
|
||||
exit 0
|
||||
else
|
||||
print_error "❌ Coverage: ${coverage}% (below minimum ${min_coverage}%)"
|
||||
exit 1
|
||||
fi
|
||||
if check_coverage_threshold "$coverage" "$min_coverage"; then
|
||||
print_success "✅ Coverage: ${coverage}%"
|
||||
exit 0
|
||||
else
|
||||
print_error "❌ Coverage: ${coverage}% (below minimum ${min_coverage}%)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_arguments "$@"
|
||||
check_lcov_file_presence
|
||||
inject_missing_files
|
||||
filter_generated_files
|
||||
parse_arguments "$@"
|
||||
check_lcov_file_presence
|
||||
inject_missing_files
|
||||
filter_generated_files
|
||||
|
||||
local coverage
|
||||
coverage=$(calculate_coverage)
|
||||
local coverage
|
||||
coverage=$(calculate_coverage)
|
||||
|
||||
if [ -z "$MIN_COVERAGE" ]; then
|
||||
echo "$coverage"
|
||||
exit 0
|
||||
fi
|
||||
if [ -z "$MIN_COVERAGE" ]; then
|
||||
echo "$coverage"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
print_coverage_result "$coverage" "$MIN_COVERAGE"
|
||||
print_coverage_result "$coverage" "$MIN_COVERAGE"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -19,10 +19,16 @@ INPUT="$1"
|
||||
# Detect format and normalize to both npub and hex
|
||||
if [[ "$INPUT" == npub1* ]]; then
|
||||
NPUB="$INPUT"
|
||||
HEX=$(nak decode "$INPUT" 2>/dev/null) || { echo "ERROR: Invalid npub"; exit 1; }
|
||||
HEX=$(nak decode "$INPUT" 2>/dev/null) || {
|
||||
echo "ERROR: Invalid npub"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
HEX="$INPUT"
|
||||
NPUB=$(nak encode npub "$INPUT" 2>/dev/null) || { echo "ERROR: Invalid hex pubkey"; exit 1; }
|
||||
NPUB=$(nak encode npub "$INPUT" 2>/dev/null) || {
|
||||
echo "ERROR: Invalid hex pubkey"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
SOURCE_RELAYS="wss://relay.primal.net wss://relay.damus.io wss://purplepag.es wss://nos.lol"
|
||||
@@ -78,12 +84,12 @@ while read -r pk; do
|
||||
[ -z "$event" ] && continue
|
||||
publish "$event" >/dev/null 2>&1
|
||||
BATCH_COUNT=$((BATCH_COUNT + 1))
|
||||
done <<< "$EVENTS"
|
||||
done <<<"$EVENTS"
|
||||
PUBLISHED=$((PUBLISHED + BATCH_COUNT))
|
||||
printf " batch %d: %d metadata events (%d total)\n" "$BATCH_NUM" "$BATCH_COUNT" "$PUBLISHED"
|
||||
BATCH_ARGS=()
|
||||
fi
|
||||
done <<< "$PUBKEYS"
|
||||
done <<<"$PUBKEYS"
|
||||
|
||||
if [ ${#BATCH_ARGS[@]} -gt 0 ]; then
|
||||
BATCH_NUM=$((BATCH_NUM + 1))
|
||||
@@ -93,7 +99,7 @@ if [ ${#BATCH_ARGS[@]} -gt 0 ]; then
|
||||
[ -z "$event" ] && continue
|
||||
publish "$event" >/dev/null 2>&1
|
||||
BATCH_COUNT=$((BATCH_COUNT + 1))
|
||||
done <<< "$EVENTS"
|
||||
done <<<"$EVENTS"
|
||||
PUBLISHED=$((PUBLISHED + BATCH_COUNT))
|
||||
printf " batch %d: %d metadata events (%d total)\n" "$BATCH_NUM" "$BATCH_COUNT" "$PUBLISHED"
|
||||
fi
|
||||
|
||||
@@ -7,44 +7,47 @@ echo "🔧 Setting up Android Cargo configuration..."
|
||||
|
||||
# Function to print colored output
|
||||
print_success() {
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
echo -e "\033[1;32m✅ $1\033[0m"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
echo -e "\033[1;31m❌ $1\033[0m"
|
||||
}
|
||||
|
||||
# Detect host OS for NDK toolchain path
|
||||
case "$(uname -s)" in
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
MINGW*|CYGWIN*|MSYS*) HOST_TAG="windows-x86_64" ;;
|
||||
*) print_error "Unsupported host OS: $(uname -s)"; exit 1 ;;
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
MINGW* | CYGWIN* | MSYS*) HOST_TAG="windows-x86_64" ;;
|
||||
*)
|
||||
print_error "Unsupported host OS: $(uname -s)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Auto-detect Android NDK
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
if [ -n "$ANDROID_HOME" ]; then
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ANDROID_HOME" ]; then
|
||||
NDK_DIR="$ANDROID_HOME/ndk"
|
||||
if [ -d "$NDK_DIR" ]; then
|
||||
NDK_VERSION=$(ls "$NDK_DIR" | sort -V | tail -n 1)
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
ANDROID_NDK_HOME="$NDK_DIR/$NDK_VERSION"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ANDROID_NDK_HOME" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then
|
||||
print_error "Android NDK not found. Please set ANDROID_NDK_HOME environment variable"
|
||||
exit 1
|
||||
print_error "Android NDK not found. Please set ANDROID_NDK_HOME environment variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create .cargo directory if it doesn't exist
|
||||
mkdir -p rust/.cargo
|
||||
|
||||
# Generate config.toml
|
||||
cat > rust/.cargo/config.toml << EOF
|
||||
cat >rust/.cargo/config.toml <<EOF
|
||||
[target.aarch64-linux-android]
|
||||
ar = "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${HOST_TAG}/bin/llvm-ar"
|
||||
linker = "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${HOST_TAG}/bin/aarch64-linux-android33-clang"
|
||||
|
||||
+14
-14
@@ -36,9 +36,9 @@ html_escape() {
|
||||
}
|
||||
|
||||
format_date() {
|
||||
date -u -d "$1" '+%B %d, %Y at %H:%M UTC' 2>/dev/null \
|
||||
|| TZ=UTC date -jf '%Y-%m-%dT%H:%M:%SZ' "$1" '+%B %d, %Y at %H:%M UTC' 2>/dev/null \
|
||||
|| echo "$1"
|
||||
date -u -d "$1" '+%B %d, %Y at %H:%M UTC' 2>/dev/null ||
|
||||
TZ=UTC date -jf '%Y-%m-%dT%H:%M:%SZ' "$1" '+%B %d, %Y at %H:%M UTC' 2>/dev/null ||
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -53,9 +53,9 @@ if [[ -z "$RUN_ID" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HEAD_SHA=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].head_sha')
|
||||
RUN_URL=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].html_url')
|
||||
RUN_DATE=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].updated_at')
|
||||
HEAD_SHA=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].head_sha')
|
||||
RUN_URL=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].html_url')
|
||||
RUN_DATE=$(echo "$RUNS_JSON" | jq -r '.workflow_runs[0].updated_at')
|
||||
|
||||
echo " Run #$RUN_ID sha=$HEAD_SHA"
|
||||
|
||||
@@ -66,7 +66,7 @@ echo "Fetching artifacts..."
|
||||
ARTIFACTS_JSON=$(gh_api "$API/repos/$REPO/actions/runs/$RUN_ID/artifacts")
|
||||
|
||||
ARTIFACT_NAME=$(echo "$ARTIFACTS_JSON" | jq -r '.artifacts[0].name // empty')
|
||||
ARTIFACT_ID=$(echo "$ARTIFACTS_JSON" | jq -r '.artifacts[0].id // empty')
|
||||
ARTIFACT_ID=$(echo "$ARTIFACTS_JSON" | jq -r '.artifacts[0].id // empty')
|
||||
ARTIFACT_SIZE=$(echo "$ARTIFACTS_JSON" | jq -r '.artifacts[0].size_in_bytes // 0')
|
||||
ARTIFACT_EXPIRED=$(echo "$ARTIFACTS_JSON" | jq -r '.artifacts[0].expired // false')
|
||||
|
||||
@@ -91,10 +91,10 @@ echo " Artifact: $ARTIFACT_NAME ($ARTIFACT_SIZE_MB MB)"
|
||||
echo "Fetching commit info for $HEAD_SHA..."
|
||||
COMMIT_JSON=$(gh_api "$API/repos/$REPO/commits/$HEAD_SHA")
|
||||
|
||||
COMMIT_MSG=$(echo "$COMMIT_JSON" | jq -r '.commit.message' | head -1)
|
||||
COMMIT_MSG=$(echo "$COMMIT_JSON" | jq -r '.commit.message' | head -1)
|
||||
COMMIT_AUTHOR=$(echo "$COMMIT_JSON" | jq -r '.commit.author.name')
|
||||
COMMIT_AUTHOR_ESCAPED=$(html_escape "$COMMIT_AUTHOR")
|
||||
COMMIT_DATE=$(echo "$COMMIT_JSON" | jq -r '.commit.author.date')
|
||||
COMMIT_DATE=$(echo "$COMMIT_JSON" | jq -r '.commit.author.date')
|
||||
COMMIT_URL="https://github.com/$REPO/commit/$HEAD_SHA"
|
||||
SHORT_SHA="${HEAD_SHA:0:7}"
|
||||
|
||||
@@ -236,7 +236,7 @@ PRETTY_COMMIT_DATE=$(format_date "$COMMIT_DATE")
|
||||
# Escape commit message for safe HTML embedding
|
||||
COMMIT_MSG_ESCAPED=$(html_escape "$COMMIT_MSG")
|
||||
|
||||
cat > "$OUT_DIR/index.html" <<'HTMLEOF_PART1'
|
||||
cat >"$OUT_DIR/index.html" <<'HTMLEOF_PART1'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -577,7 +577,7 @@ cat > "$OUT_DIR/index.html" <<'HTMLEOF_PART1'
|
||||
HTMLEOF_PART1
|
||||
|
||||
# --- inject dynamic master build values ---
|
||||
cat >> "$OUT_DIR/index.html" <<HTMLEOF_PART2
|
||||
cat >>"$OUT_DIR/index.html" <<HTMLEOF_PART2
|
||||
<a class="nes-btn" href="${NIGHTLY_LINK_URL}">
|
||||
★ DOWNLOAD APK ★
|
||||
</a>
|
||||
@@ -646,11 +646,11 @@ HTMLEOF_PART2
|
||||
|
||||
# --- inject PR builds ---
|
||||
if [[ -n "$PR_BUILDS_HTML" ]]; then
|
||||
cat >> "$OUT_DIR/index.html" <<HTMLEOF_PR_BUILDS
|
||||
cat >>"$OUT_DIR/index.html" <<HTMLEOF_PR_BUILDS
|
||||
${PR_BUILDS_HTML}
|
||||
HTMLEOF_PR_BUILDS
|
||||
else
|
||||
cat >> "$OUT_DIR/index.html" <<'HTMLEOF_PR_EMPTY'
|
||||
cat >>"$OUT_DIR/index.html" <<'HTMLEOF_PR_EMPTY'
|
||||
<div class="nes-box is-dark">
|
||||
<p class="pr-empty">
|
||||
No active PR builds right now.<br>
|
||||
@@ -661,7 +661,7 @@ HTMLEOF_PR_EMPTY
|
||||
fi
|
||||
|
||||
# --- close out the page ---
|
||||
cat >> "$OUT_DIR/index.html" <<HTMLEOF_PART3
|
||||
cat >>"$OUT_DIR/index.html" <<HTMLEOF_PART3
|
||||
</div>
|
||||
|
||||
<div class="pixel-divider">████████████████████████████████</div>
|
||||
|
||||
Reference in New Issue
Block a user