feat: Add icon generation script and update README with new functionality
Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions
Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions
This commit is contained in:
parent
032d9e6c31
commit
3f9fa06fbd
6 changed files with 204 additions and 17 deletions
|
|
@ -10,11 +10,36 @@ Automation scripts for building, releasing, and downloading WebDrop Bridge.
|
|||
| `download_release.sh` | Download installer from Forgejo via wget | macOS/Linux |
|
||||
| `build_windows.py` | Build Windows MSI installer | Windows |
|
||||
| `build_macos.sh` | Build macOS DMG installer | macOS |
|
||||
| `generate_icons.py` | Generate `.ico` + `.icns` from one PNG | All (macOS required for `.icns`) |
|
||||
| `create_release.ps1` | Create GitHub/Forgejo release | Windows |
|
||||
| `create_release.sh` | Create GitHub/Forgejo release | macOS/Linux |
|
||||
| `sync_remotes.ps1` | Sync git remotes | Windows |
|
||||
| `sync_version.py` | Manage version synchronization | All |
|
||||
|
||||
## Icon Generation
|
||||
|
||||
Use one master icon PNG and generate both platform formats:
|
||||
|
||||
```bash
|
||||
python build/scripts/generate_icons.py
|
||||
```
|
||||
|
||||
Defaults:
|
||||
- Source PNG: `resources/icons/app.png`
|
||||
- Windows icon: `resources/icons/app.ico`
|
||||
- macOS icon: `resources/icons/app.icns`
|
||||
|
||||
Generate only one format:
|
||||
|
||||
```bash
|
||||
python build/scripts/generate_icons.py --only ico
|
||||
python build/scripts/generate_icons.py --only icns
|
||||
```
|
||||
|
||||
Requirements:
|
||||
- `Pillow` for `.ico` generation (`pip install -r requirements-dev.txt`)
|
||||
- macOS `sips` + `iconutil` for `.icns`
|
||||
|
||||
## Download Scripts
|
||||
|
||||
### Purpose
|
||||
|
|
|
|||
|
|
@ -274,39 +274,50 @@ create_dmg() {
|
|||
else
|
||||
log_warning "create-dmg not found, using hdiutil (less stylish)"
|
||||
log_info "For professional DMG: brew install create-dmg"
|
||||
|
||||
# Create temporary DMG directory structure
|
||||
DMG_TEMP="$TEMP_BUILD/dmg_contents"
|
||||
mkdir -p "$DMG_TEMP"
|
||||
|
||||
# Copy app bundle
|
||||
cp -r "$DIST_DIR/$APP_NAME.app" "$DMG_TEMP/"
|
||||
|
||||
# Create symlink to Applications folder
|
||||
ln -s /Applications "$DMG_TEMP/Applications"
|
||||
|
||||
# Build a small, HFS+ staging image first and then convert to
|
||||
# compressed UDZO. This avoids oversized APFS container images.
|
||||
# Build a writable HFS+ staging image, mount it, and copy the
|
||||
# app bundle with ditto so symlinks are preserved exactly.
|
||||
STAGING_DMG="$TEMP_BUILD/${APP_NAME}-staging.dmg"
|
||||
STAGING_MOUNT="$TEMP_BUILD/dmg_mount"
|
||||
STAGING_EMPTY="$TEMP_BUILD/dmg_empty"
|
||||
rm -f "$STAGING_DMG"
|
||||
rm -rf "$STAGING_MOUNT"
|
||||
rm -rf "$STAGING_EMPTY"
|
||||
mkdir -p "$STAGING_MOUNT"
|
||||
mkdir -p "$STAGING_EMPTY"
|
||||
|
||||
APP_SIZE_KB=$(du -sk "$DIST_DIR/$APP_NAME.app" | awk '{print $1}')
|
||||
DMG_SIZE_KB=$((APP_SIZE_KB + 512000))
|
||||
|
||||
hdiutil create \
|
||||
-volname "$DMG_VOLUME_NAME" \
|
||||
-srcfolder "$DMG_TEMP" \
|
||||
-size "${DMG_SIZE_KB}k" \
|
||||
-fs HFS+ \
|
||||
-fsargs "-c c=64,a=16,e=16" \
|
||||
-volname "$DMG_VOLUME_NAME" \
|
||||
-format UDRW \
|
||||
-srcfolder "$STAGING_EMPTY" \
|
||||
-ov \
|
||||
"$STAGING_DMG"
|
||||
|
||||
hdiutil attach "$STAGING_DMG" \
|
||||
-mountpoint "$STAGING_MOUNT" \
|
||||
-nobrowse \
|
||||
-noverify \
|
||||
-noautoopen
|
||||
|
||||
ditto "$DIST_DIR/$APP_NAME.app" "$STAGING_MOUNT/$APP_NAME.app"
|
||||
ln -s /Applications "$STAGING_MOUNT/Applications"
|
||||
|
||||
hdiutil detach "$STAGING_MOUNT"
|
||||
|
||||
hdiutil convert "$STAGING_DMG" \
|
||||
-format UDZO \
|
||||
-imagekey zlib-level=9 \
|
||||
-o "$DMG_FILE"
|
||||
|
||||
|
||||
# Clean up
|
||||
rm -f "$STAGING_DMG"
|
||||
rm -rf "$DMG_TEMP"
|
||||
rm -rf "$STAGING_MOUNT"
|
||||
rm -rf "$STAGING_EMPTY"
|
||||
fi
|
||||
|
||||
if [ ! -f "$DMG_FILE" ]; then
|
||||
|
|
|
|||
150
build/scripts/generate_icons.py
Normal file
150
build/scripts/generate_icons.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate application icons for Windows (.ico) and macOS (.icns).
|
||||
|
||||
This script creates both platform icon formats from a single master PNG so
|
||||
branding stays visually consistent across installers.
|
||||
|
||||
Requirements:
|
||||
- Pillow (for .ico generation)
|
||||
- macOS tools `sips` and `iconutil` (for .icns generation)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ICONSET_ENTRIES: list[tuple[str, int]] = [
|
||||
("icon_16x16.png", 16),
|
||||
("icon_16x16@2x.png", 32),
|
||||
("icon_32x32.png", 32),
|
||||
("icon_32x32@2x.png", 64),
|
||||
("icon_128x128.png", 128),
|
||||
("icon_128x128@2x.png", 256),
|
||||
("icon_256x256.png", 256),
|
||||
("icon_256x256@2x.png", 512),
|
||||
("icon_512x512.png", 512),
|
||||
("icon_512x512@2x.png", 1024),
|
||||
]
|
||||
|
||||
ICO_SIZES = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> None:
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
def _require_tool(name: str) -> None:
|
||||
if shutil.which(name):
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"Required tool '{name}' not found in PATH. "
|
||||
f"Install it and retry."
|
||||
)
|
||||
|
||||
|
||||
def generate_icns(source_png: Path, target_icns: Path) -> None:
|
||||
"""Generate macOS .icns using iconutil and sips."""
|
||||
_require_tool("sips")
|
||||
_require_tool("iconutil")
|
||||
|
||||
target_icns.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="webdrop_iconset_") as tmp:
|
||||
iconset_dir = Path(tmp) / "app.iconset"
|
||||
iconset_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for filename, size in ICONSET_ENTRIES:
|
||||
out_file = iconset_dir / filename
|
||||
_run(
|
||||
[
|
||||
"sips",
|
||||
"-z",
|
||||
str(size),
|
||||
str(size),
|
||||
str(source_png),
|
||||
"--out",
|
||||
str(out_file),
|
||||
]
|
||||
)
|
||||
|
||||
_run(["iconutil", "-c", "icns", str(iconset_dir), "-o", str(target_icns)])
|
||||
|
||||
|
||||
def generate_ico(source_png: Path, target_ico: Path) -> None:
|
||||
"""Generate Windows .ico using Pillow."""
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"Pillow is required for .ico generation. "
|
||||
"Install it with: pip install Pillow"
|
||||
) from exc
|
||||
|
||||
target_ico.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with Image.open(source_png) as img:
|
||||
rgba = img.convert("RGBA")
|
||||
rgba.save(target_ico, format="ICO", sizes=ICO_SIZES)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate .ico and .icns icons from a single PNG source"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
default="resources/icons/app.png",
|
||||
help="Master source PNG (default: resources/icons/app.png)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--icon-ico",
|
||||
default="resources/icons/app.ico",
|
||||
help="Target .ico path (default: resources/icons/app.ico)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--icon-icns",
|
||||
default="resources/icons/app.icns",
|
||||
help="Target .icns path (default: resources/icons/app.icns)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only",
|
||||
choices=["all", "ico", "icns"],
|
||||
default="all",
|
||||
help="Generate all icons or only one format",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
source = Path(args.source).resolve()
|
||||
icon_ico = Path(args.icon_ico).resolve()
|
||||
icon_icns = Path(args.icon_icns).resolve()
|
||||
|
||||
if not source.exists():
|
||||
print(f"ERROR: Source icon not found: {source}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
if args.only in {"all", "ico"}:
|
||||
generate_ico(source, icon_ico)
|
||||
print(f"OK: Generated ICO: {icon_ico}")
|
||||
|
||||
if args.only in {"all", "icns"}:
|
||||
generate_icns(source, icon_icns)
|
||||
print(f"OK: Generated ICNS: {icon_icns}")
|
||||
|
||||
return 0
|
||||
except (subprocess.CalledProcessError, RuntimeError) as exc:
|
||||
print(f"ERROR: {exc}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue