4.1.4: QR sharing, venv tarball, flash script improvements

QR Channel Key Sharing:
- Admin-only QR generator in about.html (was visible to all)
- QR button for saved keys on account page
- Fixed about() route missing channel status vars (bug)

Pi Build Optimization:
- Pre-built venv tarball support (39MB zstd, skips 20+ min compile)
- setup.sh auto-detects and extracts tarball if present
- Strip __pycache__/tests before tarball (295MB → 208MB)

Flash Script Improvements:
- flash-image.sh now uses config.json for headless WiFi setup
- Consistent wipe prompt on both flash scripts
- pull-image.sh re-enables auto-expand before shrinking

Build Docs:
- Added zstd and jq to pre-setup apt-get
- Documented fast build option with pre-built venv

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-06 15:03:46 -05:00
parent cc46993d80
commit d58f3c6fb6
9 changed files with 513 additions and 48 deletions

3
.gitignore vendored
View File

@@ -85,3 +85,6 @@ pishrink.sh
# Temp file storage # Temp file storage
frontends/web/temp_files/ frontends/web/temp_files/
rpi/config.json rpi/config.json
# Pre-built venv tarball (39MB - too large for git)
rpi/stegasoo-venv-pi-arm64.tar.zst

View File

@@ -1,17 +1,157 @@
# Stegasoo 4.1.4 Plan # Stegasoo 4.1.4 Plan
## Build / Deploy ## Build / Deploy
- [ ] Pre-built Python 3.12 venv tarball for Pi (skip 20+ min compile) - [x] Pre-built Python 3.12 venv tarball for Pi (skip 20+ min compile) - see details below
- [x] Fixed partition sizing in flash script (16GB rootfs for faster imaging) - [x] Fixed partition sizing in flash script (16GB rootfs for faster imaging)
- [x] Rename `flash-pi.sh``flash-stock-img.sh` for clarity - [x] Rename `flash-pi.sh``flash-stock-img.sh` for clarity
- [x] pip-audit integration in release validation - [x] pip-audit integration in release validation
### Pi venv Tarball Approach
1. Flash fresh Pi image, let it fully build (20+ min compile)
2. Once running and working, SSH in and create optimized tarball:
```bash
cd /opt/stegasoo
# Strip caches and tests (295MB → 208MB)
find venv/ -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null
find venv/ -type d -name 'tests' -exec rm -rf {} + 2>/dev/null
find venv/ -type d -name 'test' -exec rm -rf {} + 2>/dev/null
# Compress with zstd (208MB → 39MB)
tar -cf - venv/ | zstd -19 -T0 > /tmp/stegasoo-venv-pi-arm64.tar.zst
```
3. Pull tarball to host: `scp admin@pi:/tmp/stegasoo-venv-pi-arm64.tar.zst rpi/`
4. setup.sh auto-detects and extracts tarball if present in rpi/
5. Re-flash and test fresh build with pre-built venv (should be <2 min vs 20+)
## Features ## Features
- [ ] QR channel key sharing (needs UI thought - avoid crowding encode/decode pages) - [x] QR channel key sharing (see detailed plan below)
- [ ] Role-based permissions: admin / mod / user - [ ] Role-based permissions: admin / mod / user
- [x] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.) - [x] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.)
- [ ] Better capacity estimates / pre-flight check before encode fails - [ ] Better capacity estimates / pre-flight check before encode fails
---
## QR Channel Key Sharing - Implementation Plan
### Current State
- ✅ **CLI**: `stegasoo channel qr` generates ASCII/PNG QR for server channel key
- ✅ **Web UI (about.html)**: Client-side QR generator exists - input key, generate/show QR, download PNG
- ✅ **Account page**: Shows saved channel keys with fingerprint, rename, delete
- ❌ No role restrictions on QR sharing
- ❌ No QR button for saved keys on account page
- ❌ No QR scanning to import keys
### Design Decisions
**UI Placement** (avoiding encode/decode page crowding):
- Keep QR generator in **about.html** (already exists, logical place for tools)
- Add QR button to **account.html** saved keys (small icon, doesn't crowd)
- Both should be admin-only
**Role Restriction** (per user request):
- QR sharing = admin only (hide generator + saved key QR buttons from non-admins)
- Prerequisite: Need role-based permissions feature first
- Interim option: Just hide from non-admin users using existing `is_admin` flag
### Implementation Steps
#### Phase 1: Admin-only restriction (quick win)
1. **about.html**: Wrap QR generator section in `{% if is_admin %}` block
2. **Account route**: Pass `is_admin` to template (if not already)
3. **account.html**: Add small QR icon button to saved keys row (admin only)
- Opens modal with QR canvas (reuse qrcode.js pattern from about.html)
- Download PNG button in modal
#### Phase 2: QR Import (optional enhancement)
1. Add "Import via QR" button to account.html key-add section
2. Use device camera or file upload to scan QR
3. Decode and populate channel_key input field
4. Requires `pyzbar` on server OR client-side JS library like `jsQR`
### Files to Modify
```
frontends/web/app.py
- about() route: Add missing vars: is_admin, channel_configured,
channel_fingerprint, channel_source (BUG: currently not passed!)
- account() route: ✅ Already passes is_admin
frontends/web/templates/about.html
- Wrap channel key QR section in {% if is_admin %}
frontends/web/templates/account.html
- Add QR button to saved keys (admin only)
- Add QR modal (copy pattern from about.html)
- Include qrcode.min.js CDN script
```
### Bug Found During Research
The about.html template uses `channel_configured`, `channel_fingerprint`,
`channel_source` but the route doesn't pass them - always shows "public mode".
Fix this while implementing QR admin restriction.
### Exact Code Changes
**app.py - Fix about() route (around line 1564):**
```python
@app.route("/about")
def about():
from stegasoo.channel import get_channel_status
channel_status = get_channel_status()
# Check if user is admin (for QR sharing)
current_user = get_current_user()
is_admin = current_user.is_admin if current_user else False
return render_template(
"about.html",
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
# Channel info (bugfix)
channel_configured=channel_status["configured"],
channel_fingerprint=channel_status.get("fingerprint"),
channel_source=channel_status.get("source"),
# Admin check for QR sharing
is_admin=is_admin,
)
```
### Template Changes Preview
**account.html - Add to saved key row:**
```html
{% if is_admin %}
<button type="button" class="btn btn-outline-info btn-sm"
onclick="showKeyQr('{{ key.channel_key }}')" title="Show QR">
<i class="bi bi-qr-code"></i>
</button>
{% endif %}
```
**about.html - Wrap existing section:**
```html
{% if is_admin %}
<!-- Channel Key QR Generator -->
<div class="card bg-dark border-secondary">
...existing QR generator...
</div>
{% endif %}
```
### Testing Checklist (Phase 1 Implemented)
- [ ] Non-admin users cannot see QR generator in about.html
- [ ] Non-admin users cannot see QR buttons on account page
- [ ] Admin users can generate QR for any saved key
- [ ] QR downloads work correctly
- [ ] QR scans correctly with phone camera
### Implementation Status
**Phase 1: COMPLETE** - Admin-only QR sharing implemented:
- `app.py`: Fixed about() route to pass channel status + is_admin
- `about.html`: QR generator wrapped in `{% if is_admin %}` with Admin badge
- `account.html`: QR button added to saved keys (admin only), modal + JS for generation/download
---
## Security ## Security
- [ ] Optional encryption for temp file storage (paranoid mode, config toggle) - [ ] Optional encryption for temp file storage (paranoid mode, config toggle)

View File

@@ -1561,7 +1561,25 @@ def decode_download(file_id):
@app.route("/about") @app.route("/about")
def about(): def about():
return render_template("about.html", has_argon2=has_argon2(), has_qrcode_read=HAS_QRCODE_READ) from stegasoo.channel import get_channel_status
channel_status = get_channel_status()
# Check if user is admin (for QR sharing)
current_user = get_current_user()
is_admin = current_user.is_admin if current_user else False
return render_template(
"about.html",
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
# Channel info (bugfix - was not being passed)
channel_configured=channel_status["configured"],
channel_fingerprint=channel_status.get("fingerprint"),
channel_source=channel_status.get("source"),
# Admin check for QR sharing
is_admin=is_admin,
)
# ============================================================================ # ============================================================================

View File

@@ -331,10 +331,12 @@
</div> </div>
{% endif %} {% endif %}
<!-- Channel Key QR Generator --> <!-- Channel Key QR Generator (Admin only) -->
{% if is_admin %}
<div class="card bg-dark border-secondary"> <div class="card bg-dark border-secondary">
<div class="card-header"> <div class="card-header">
<i class="bi bi-qr-code me-2"></i>Share Channel Key via QR <i class="bi bi-qr-code me-2"></i>Share Channel Key via QR
<span class="badge bg-warning text-dark ms-2"><i class="bi bi-shield-check me-1"></i>Admin</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p> <p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
@@ -366,6 +368,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>

View File

@@ -140,6 +140,13 @@
{% endif %} {% endif %}
</div> </div>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
{% if is_admin %}
<button type="button" class="btn btn-outline-info"
onclick="showKeyQr('{{ key.channel_key }}', '{{ key.name }}')"
title="Show QR Code">
<i class="bi bi-qr-code"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-secondary" <button type="button" class="btn btn-outline-secondary"
onclick="renameKey({{ key.id }}, '{{ key.name }}')" onclick="renameKey({{ key.id }}, '{{ key.name }}')"
title="Rename"> title="Rename">
@@ -218,10 +225,38 @@
</div> </div>
</div> </div>
</div> </div>
{% if is_admin %}
<!-- QR Code Modal (Admin only) -->
<div class="modal fade" id="qrModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title"><i class="bi bi-qr-code me-2"></i><span id="qrKeyName">Channel Key</span></h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<canvas id="qrCanvas" class="bg-white p-2 rounded"></canvas>
<div class="mt-2">
<code class="small" id="qrKeyDisplay"></code>
</div>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrDownload">
<i class="bi bi-download me-1"></i>Download PNG
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script> <script src="{{ url_for('static', filename='js/auth.js') }}"></script>
{% if is_admin %}
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
{% endif %}
<script> <script>
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput'); StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
@@ -230,5 +265,45 @@ function renameKey(keyId, currentName) {
document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename'; document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename';
new bootstrap.Modal(document.getElementById('renameModal')).show(); new bootstrap.Modal(document.getElementById('renameModal')).show();
} }
{% if is_admin %}
function showKeyQr(channelKey, keyName) {
// Format key with dashes if not already
const clean = channelKey.replace(/-/g, '').toUpperCase();
const formatted = clean.match(/.{4}/g)?.join('-') || clean;
// Update modal content
document.getElementById('qrKeyName').textContent = keyName;
document.getElementById('qrKeyDisplay').textContent = formatted;
// Generate QR code
const canvas = document.getElementById('qrCanvas');
if (typeof QRCode !== 'undefined' && canvas) {
QRCode.toCanvas(canvas, formatted, {
width: 200,
margin: 2,
color: { dark: '#000', light: '#fff' }
}, function(error) {
if (error) {
console.error('QR generation error:', error);
return;
}
new bootstrap.Modal(document.getElementById('qrModal')).show();
});
}
}
// Download QR as PNG
document.getElementById('qrDownload')?.addEventListener('click', function() {
const canvas = document.getElementById('qrCanvas');
const keyName = document.getElementById('qrKeyName').textContent;
if (canvas) {
const link = document.createElement('a');
link.download = 'stegasoo-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
});
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -26,8 +26,8 @@ ssh admin@stegasoo.local
# Take ownership of /opt (for pyenv, jpegio builds) # Take ownership of /opt (for pyenv, jpegio builds)
sudo chown admin:admin /opt sudo chown admin:admin /opt
# Install git (not included in Lite image) # Install git and zstd (not included in Lite image)
sudo apt-get update && sudo apt-get install -y git sudo apt-get update && sudo apt-get install -y git zstd jq
``` ```
## Step 4: Clone & Run Setup ## Step 4: Clone & Run Setup
@@ -39,7 +39,22 @@ cd stegasoo
./rpi/setup.sh ./rpi/setup.sh
``` ```
This takes ~15-20 minutes and installs: ### Fast Build Option (with pre-built venv)
If you have `stegasoo-venv-pi-arm64.tar.zst` from a previous build:
```bash
cd /opt
git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo
# Copy pre-built venv (from your host machine)
# On host: scp rpi/stegasoo-venv-pi-arm64.tar.zst admin@stegasoo.local:/opt/stegasoo/rpi/
cd stegasoo
./rpi/setup.sh # Detects tarball, extracts instead of compiling (~2 min vs 20+)
```
**Standard build** takes ~15-20 minutes and installs:
- Python 3.12 via pyenv - Python 3.12 via pyenv
- jpegio (patched for ARM) - jpegio (patched for ARM)
- Stegasoo with web UI - Stegasoo with web UI

View File

@@ -8,9 +8,14 @@
# Supports: .img, .img.zst, .img.xz, .img.gz, .img.zst.zip (GitHub release format) # Supports: .img, .img.zst, .img.xz, .img.gz, .img.zst.zip (GitHub release format)
# If device is specified, skips auto-detection (useful for NVMe/large drives) # If device is specified, skips auto-detection (useful for NVMe/large drives)
# #
# Optional: Place config.json in same directory for headless WiFi setup
#
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
@@ -18,6 +23,28 @@ BLUE='\033[0;34m'
BOLD='\033[1m' BOLD='\033[1m'
NC='\033[0m' NC='\033[0m'
# Load config if present (optional - for headless WiFi setup)
HAS_CONFIG=false
if [ -f "$CONFIG_FILE" ] && command -v jq &> /dev/null; then
WIFI_SSID=$(jq -r '.wifiSSID // empty' "$CONFIG_FILE")
WIFI_PASS=$(jq -r '.wifiPassword // empty' "$CONFIG_FILE")
WIFI_COUNTRY=$(jq -r '.wifiCountry // "US"' "$CONFIG_FILE")
PI_HOSTNAME=$(jq -r '.hostname // empty' "$CONFIG_FILE")
if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PASS" ]; then
HAS_CONFIG=true
echo -e "${GREEN}Found config.json - will configure WiFi after flash${NC}"
echo -e " WiFi: ${YELLOW}$WIFI_SSID${NC}"
if [ -n "$PI_HOSTNAME" ]; then
echo -e " Hostname: ${YELLOW}$PI_HOSTNAME${NC}"
fi
echo ""
fi
elif [ -f "$CONFIG_FILE" ]; then
echo -e "${YELLOW}Note: config.json found but jq not installed (apt install jq)${NC}"
echo -e "${YELLOW} WiFi will need to be configured manually after boot${NC}"
echo ""
fi
# Check for required tools # Check for required tools
for cmd in dd lsblk; do for cmd in dd lsblk; do
if ! command -v $cmd &> /dev/null; then if ! command -v $cmd &> /dev/null; then
@@ -222,6 +249,17 @@ if [ -n "$MOUNTED" ]; then
done done
fi fi
# Ask about wiping
echo
read -p "Wipe partition table first? (recommended if having issues) [y/N] " wipe_confirm
if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then
echo "Wiping partition table..."
sudo wipefs -a "$SELECTED"
sudo dd if=/dev/zero of="$SELECTED" bs=1M count=10 status=none
sync
echo " Wiped clean"
fi
# Final confirmation # Final confirmation
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ WARNING: ALL DATA ON THIS DEVICE WILL BE DESTROYED! ║${NC}" echo -e "${RED}║ WARNING: ALL DATA ON THIS DEVICE WILL BE DESTROYED! ║${NC}"
@@ -284,6 +322,109 @@ echo ""
echo -e "${GREEN}Syncing...${NC}" echo -e "${GREEN}Syncing...${NC}"
sync sync
# Inject WiFi config if config.json was loaded
if [ "$HAS_CONFIG" = true ]; then
echo ""
echo -e "${GREEN}Configuring WiFi from config.json...${NC}"
# Wait for partitions to appear
sleep 2
partprobe "$SELECTED" 2>/dev/null || true
sleep 1
# Determine boot partition
if [[ "$SELECTED" == *"nvme"* ]] || [[ "$SELECTED" == *"mmcblk"* ]]; then
BOOT_PART="${SELECTED}p1"
else
BOOT_PART="${SELECTED}1"
fi
if [ -b "$BOOT_PART" ]; then
MOUNT_DIR=$(mktemp -d)
if mount "$BOOT_PART" "$MOUNT_DIR" 2>/dev/null; then
# Create firstrun.sh for WiFi setup
cat > "$MOUNT_DIR/firstrun.sh" << 'EOFSCRIPT'
#!/bin/bash
set +e
# Set hostname if provided
if [ -n "PLACEHOLDER_HOSTNAME" ] && [ "PLACEHOLDER_HOSTNAME" != "" ]; then
CURRENT_HOSTNAME=$(cat /etc/hostname | tr -d " \t\n\r")
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom set_hostname PLACEHOLDER_HOSTNAME
else
echo PLACEHOLDER_HOSTNAME >/etc/hostname
sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\tPLACEHOLDER_HOSTNAME/g" /etc/hosts
fi
fi
# Configure WiFi
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom set_wlan 'PLACEHOLDER_SSID' 'PLACEHOLDER_WIFIPASS' 'PLACEHOLDER_COUNTRY'
else
# NetworkManager method (Trixie)
cat >/etc/NetworkManager/system-connections/preconfigured.nmconnection <<'NMEOF'
[connection]
id=preconfigured
type=wifi
autoconnect=true
[wifi]
mode=infrastructure
ssid=PLACEHOLDER_SSID
[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=PLACEHOLDER_WIFIPASS
[ipv4]
method=auto
[ipv6]
method=auto
NMEOF
chmod 600 /etc/NetworkManager/system-connections/preconfigured.nmconnection
rfkill unblock wifi
fi
# Cleanup
rm -f /boot/firstrun.sh
rm -f /boot/firmware/firstrun.sh
sed -i 's| systemd.run.*||g' /boot/cmdline.txt 2>/dev/null
sed -i 's| systemd.run.*||g' /boot/firmware/cmdline.txt 2>/dev/null
exit 0
EOFSCRIPT
# Replace placeholders
sed -i "s/PLACEHOLDER_SSID/$WIFI_SSID/g" "$MOUNT_DIR/firstrun.sh"
sed -i "s/PLACEHOLDER_WIFIPASS/$WIFI_PASS/g" "$MOUNT_DIR/firstrun.sh"
sed -i "s/PLACEHOLDER_COUNTRY/$WIFI_COUNTRY/g" "$MOUNT_DIR/firstrun.sh"
if [ -n "$PI_HOSTNAME" ]; then
sed -i "s/PLACEHOLDER_HOSTNAME/$PI_HOSTNAME/g" "$MOUNT_DIR/firstrun.sh"
else
sed -i "s/PLACEHOLDER_HOSTNAME//g" "$MOUNT_DIR/firstrun.sh"
fi
chmod +x "$MOUNT_DIR/firstrun.sh"
# Update cmdline.txt to run firstrun.sh
CMDLINE="$MOUNT_DIR/cmdline.txt"
if [ -f "$CMDLINE" ]; then
CURRENT=$(cat "$CMDLINE" | tr -d '\n' | sed 's| systemd.run.*||g')
echo "$CURRENT systemd.run=/boot/firmware/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target" > "$CMDLINE"
fi
umount "$MOUNT_DIR"
echo -e " ${GREEN}${NC} WiFi configured for: $WIFI_SSID"
else
echo -e " ${YELLOW}${NC} Could not mount boot partition"
fi
rmdir "$MOUNT_DIR" 2>/dev/null || true
else
echo -e " ${YELLOW}${NC} Boot partition not found"
fi
fi
echo "" echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Flash Complete! ║${NC}" echo -e "${GREEN}║ Flash Complete! ║${NC}"
@@ -291,5 +432,11 @@ echo -e "${GREEN}╚════════════════════
echo "" echo ""
echo -e "You can now remove the SD card and boot your Raspberry Pi." echo -e "You can now remove the SD card and boot your Raspberry Pi."
echo "" echo ""
echo -e "${YELLOW}Tip:${NC} On first boot, SSH in and the setup wizard will run automatically." if [ "$HAS_CONFIG" = true ]; then
echo -e "${GREEN}WiFi pre-configured${NC} - Pi will connect to $WIFI_SSID on boot"
echo -e "SSH: ${YELLOW}ssh admin@${PI_HOSTNAME:-stegasoo}.local${NC} (password: stegasoo)"
else
echo -e "${YELLOW}Tip:${NC} On first boot, the setup wizard will help configure WiFi."
echo -e "${YELLOW}Tip:${NC} Or place config.json in rpi/ for headless setup next time."
fi
echo "" echo ""

View File

@@ -168,12 +168,41 @@ fi
DEV_SIZE=$(blockdev --getsize64 "$SELECTED") DEV_SIZE=$(blockdev --getsize64 "$SELECTED")
echo "" echo ""
echo -e "${GREEN}[1/3]${NC} Copying image from $SELECTED..." echo -e "${GREEN}[1/4]${NC} Copying image from $SELECTED..."
dd if="$SELECTED" bs=4M status=none | pv -s "$DEV_SIZE" > "$IMG_FILE" dd if="$SELECTED" bs=4M status=none | pv -s "$DEV_SIZE" > "$IMG_FILE"
sync sync
echo "" echo ""
echo -e "${GREEN}[2/3]${NC} Shrinking image..." echo -e "${GREEN}[2/4]${NC} Re-enabling auto-expand for distribution..."
# Mount the image and restore auto-expand service (may have been disabled during build)
LOOP_DEV=$(losetup -f --show -P "$IMG_FILE")
if [ -n "$LOOP_DEV" ]; then
TEMP_MOUNT=$(mktemp -d)
if mount "${LOOP_DEV}p2" "$TEMP_MOUNT" 2>/dev/null; then
# Re-enable the resize service if the service file exists
SERVICE_FILE="$TEMP_MOUNT/lib/systemd/system/rpi-resizerootfs.service"
SERVICE_LINK="$TEMP_MOUNT/etc/systemd/system/multi-user.target.wants/rpi-resizerootfs.service"
if [ -f "$SERVICE_FILE" ] && [ ! -L "$SERVICE_LINK" ]; then
mkdir -p "$(dirname "$SERVICE_LINK")"
ln -sf /lib/systemd/system/rpi-resizerootfs.service "$SERVICE_LINK"
echo -e " ${GREEN}${NC} Auto-expand service re-enabled"
elif [ -L "$SERVICE_LINK" ]; then
echo -e " ${GREEN}${NC} Auto-expand already enabled"
else
echo -e " ${YELLOW}${NC} Could not find resize service file"
fi
umount "$TEMP_MOUNT"
else
echo -e " ${YELLOW}${NC} Could not mount rootfs, skipping auto-expand fix"
fi
rmdir "$TEMP_MOUNT" 2>/dev/null || true
losetup -d "$LOOP_DEV"
else
echo -e " ${YELLOW}${NC} Could not create loop device, skipping auto-expand fix"
fi
echo ""
echo -e "${GREEN}[3/4]${NC} Shrinking image..."
if command -v pishrink.sh &> /dev/null; then if command -v pishrink.sh &> /dev/null; then
pishrink.sh "$IMG_FILE" pishrink.sh "$IMG_FILE"
elif [ -f "./pishrink.sh" ]; then elif [ -f "./pishrink.sh" ]; then
@@ -187,11 +216,11 @@ fi
echo "" echo ""
if [ "$SKIP_COMPRESS" = true ]; then if [ "$SKIP_COMPRESS" = true ]; then
echo -e "${GREEN}[3/3]${NC} Skipping compression (.img output)" echo -e "${GREEN}[4/4]${NC} Skipping compression (.img output)"
FINAL_SIZE=$(du -h "$IMG_FILE" | awk '{print $1}') FINAL_SIZE=$(du -h "$IMG_FILE" | awk '{print $1}')
OUTPUT="$IMG_FILE" OUTPUT="$IMG_FILE"
else else
echo -e "${GREEN}[3/3]${NC} Compressing with zstd..." echo -e "${GREEN}[4/4]${NC} Compressing with zstd..."
pv "$IMG_FILE" | zstd -19 -T0 -q > "$OUTPUT" pv "$IMG_FILE" | zstd -19 -T0 -q > "$OUTPUT"
rm -f "$IMG_FILE" rm -f "$IMG_FILE"
FINAL_SIZE=$(du -h "$OUTPUT" | awk '{print $1}') FINAL_SIZE=$(du -h "$OUTPUT" | awk '{print $1}')

View File

@@ -135,6 +135,7 @@ sudo apt-get install -y \
build-essential \ build-essential \
git \ git \
curl \ curl \
zstd \
libssl-dev \ libssl-dev \
zlib1g-dev \ zlib1g-dev \
libbz2-dev \ libbz2-dev \
@@ -218,50 +219,84 @@ else
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
fi fi
echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..." # Check for pre-built venv tarball (skips 20+ min compile time)
PREBUILT_VENV="$INSTALL_DIR/rpi/stegasoo-venv-pi-arm64.tar.zst"
PREBUILT_VENV_URL="${PREBUILT_VENV_URL:-}" # Optional: URL to download from
# Create venv with pyenv Python (not system Python) if [ -f "$PREBUILT_VENV" ] || [ -n "$PREBUILT_VENV_URL" ]; then
# Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping) echo -e "${GREEN}[6/8]${NC} Installing pre-built Python environment..."
PYENV_PYTHON=$(pyenv which python)
echo " Using Python: $PYENV_PYTHON"
if [ ! -d "venv" ]; then
"$PYENV_PYTHON" -m venv venv
fi
source venv/bin/activate
# Verify we're using the right Python # Download if URL provided and local file doesn't exist
VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2) if [ ! -f "$PREBUILT_VENV" ] && [ -n "$PREBUILT_VENV_URL" ]; then
echo " venv Python: $VENV_PY" echo " Downloading pre-built venv..."
curl -L -o "$PREBUILT_VENV" "$PREBUILT_VENV_URL"
fi
echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..." # Extract pre-built venv (zstd compressed)
echo " Extracting pre-built venv (this is much faster!)..."
zstd -d "$PREBUILT_VENV" --stdout | tar -xf - -C "$INSTALL_DIR"
# Clone jpegio # Activate and verify
JPEGIO_DIR="/tmp/jpegio-build" source venv/bin/activate
rm -rf "$JPEGIO_DIR" VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
git clone "$JPEGIO_REPO" "$JPEGIO_DIR" echo -e " ${GREEN}${NC} venv Python: $VENV_PY"
# Apply ARM64 patch # Install stegasoo package in editable mode (quick, no compile)
if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then echo -e "${GREEN}[7/8]${NC} Installing Stegasoo package..."
bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR" pip install -e "." --quiet
# Adjust step numbers for rest of script
STEP_OFFSET=-4
else else
echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..."
echo -e " ${YELLOW}Note: No pre-built venv found. Building from source (20+ min)${NC}"
echo -e " ${YELLOW}To speed up future installs, add stegasoo-venv-pi-arm64.tar.gz to rpi/${NC}"
# Create venv with pyenv Python (not system Python)
# Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping)
PYENV_PYTHON=$(pyenv which python)
echo " Using Python: $PYENV_PYTHON"
if [ ! -d "venv" ]; then
"$PYENV_PYTHON" -m venv venv
fi
source venv/bin/activate
# Verify we're using the right Python
VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
echo " venv Python: $VENV_PY"
echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..."
# Clone jpegio
JPEGIO_DIR="/tmp/jpegio-build"
rm -rf "$JPEGIO_DIR"
git clone "$JPEGIO_REPO" "$JPEGIO_DIR"
# Apply ARM64 patch
if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then
bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR"
else
echo " Applying inline ARM64 patch..." echo " Applying inline ARM64 patch..."
sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py" sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py"
fi
cd "$JPEGIO_DIR"
# Build jpegio into venv
pip install --upgrade pip setuptools wheel cython numpy
pip install .
cd "$INSTALL_DIR"
rm -rf "$JPEGIO_DIR"
echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..."
# Install dependencies (jpegio already in venv, won't re-download)
pip install -e ".[web]"
STEP_OFFSET=0
fi fi
cd "$JPEGIO_DIR"
# Build jpegio into venv
pip install --upgrade pip setuptools wheel cython numpy
pip install .
cd "$INSTALL_DIR"
rm -rf "$JPEGIO_DIR"
echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..."
# Install dependencies (jpegio already in venv, won't re-download)
pip install -e ".[web]"
echo -e "${GREEN}[9/12]${NC} Creating systemd service..." echo -e "${GREEN}[9/12]${NC} Creating systemd service..."
# Create systemd service file # Create systemd service file