#!/usr/bin/env bash # php-qml-init — scaffold a fresh php-qml application. # # Copies framework/skeleton/ into /, rewrites the identifiers # (project name, QML module URI, single-instance lock id, app title), # repoints the path-composer-repo at this framework checkout (or a # vendored copy if --vendor is given), runs composer install, and # prints the make targets the user is expected to run next. # # Usage: # php-qml-init [--framework ] [--vendor] [--skip-install] # [--git] # # Curl-bootstrap pattern (when this repo is your origin): # curl -fsSL /bin/php-qml-init | bash -s -- \ # --framework "$(pwd)/php-qml" my-app # # The framework is auto-detected when this script is run from a checkout # (i.e. lives at /bin/php-qml-init); otherwise pass --framework # or set PHP_QML_FRAMEWORK in the environment. set -euo pipefail # ── Logging helpers ────────────────────────────────────────────────── say() { printf '→ %s\n' "$*"; } warn() { printf '! %s\n' "$*" >&2; } die() { printf '✗ %s\n' "$*" >&2; exit 1; } # ── Argument parsing ───────────────────────────────────────────────── FRAMEWORK="${PHP_QML_FRAMEWORK:-}" VENDOR=0 SKIP_INSTALL=0 GIT_INIT=0 NAME="" usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-0}" } while [ $# -gt 0 ]; do case "$1" in -h|--help) usage 0 ;; --framework) FRAMEWORK="${2:?--framework requires a path}"; shift 2 ;; --framework=*) FRAMEWORK="${1#*=}"; shift ;; --vendor) VENDOR=1; shift ;; --skip-install) SKIP_INSTALL=1; shift ;; --git) GIT_INIT=1; shift ;; --) shift; break ;; -*) die "unknown flag: $1" ;; *) if [ -z "$NAME" ]; then NAME="$1"; else die "unexpected argument: $1"; fi shift ;; esac done [ -n "$NAME" ] || { warn "missing "; usage 1; } [[ "$NAME" =~ ^[a-z][a-z0-9_-]*$ ]] \ || die "invalid name '$NAME' — use lowercase letters, digits, _ or -, leading letter" # ── Resolve framework source ───────────────────────────────────────── script_dir() { # readlink -f resolves symlinks; portable across linuxes we ship to. local s="${BASH_SOURCE[0]}" cd "$(dirname "$(readlink -f "$s")")" && pwd } if [ -z "$FRAMEWORK" ]; then candidate="$(script_dir)/.." if [ -d "$candidate/framework/skeleton" ]; then FRAMEWORK="$(cd "$candidate" && pwd)" fi fi [ -n "$FRAMEWORK" ] || die "framework dir unknown — pass --framework or set PHP_QML_FRAMEWORK" [ -d "$FRAMEWORK/framework/skeleton" ] \ || die "no framework/skeleton/ under '$FRAMEWORK' — wrong --framework path?" [ -d "$FRAMEWORK/framework/php" ] \ || die "no framework/php/ under '$FRAMEWORK' — wrong --framework path?" FRAMEWORK="$(cd "$FRAMEWORK" && pwd)" # ── Target directory must not pre-exist with content ───────────────── TARGET="$(pwd)/$NAME" if [ -e "$TARGET" ]; then [ -d "$TARGET" ] || die "$NAME exists and is not a directory" if [ -n "$(ls -A "$TARGET" 2>/dev/null)" ]; then die "$TARGET already exists and is non-empty" fi fi # ── Copy skeleton ──────────────────────────────────────────────────── say "copying skeleton → $TARGET" mkdir -p "$TARGET" # rsync gives us cross-platform-friendly excludes; fall back to cp -R if # rsync isn't installed (rare on linux, but possible on stripped images). if command -v rsync >/dev/null 2>&1; then rsync -a \ --exclude 'symfony/vendor/' \ --exclude 'symfony/var/cache/' \ --exclude 'symfony/var/log/' \ --exclude 'symfony/var/data.sqlite*' \ --exclude 'symfony/composer.lock' \ --exclude 'build/' \ "$FRAMEWORK/framework/skeleton/" "$TARGET/" else warn "rsync not found, falling back to cp -R (no excludes)" cp -R "$FRAMEWORK/framework/skeleton/." "$TARGET/" rm -rf "$TARGET/symfony/vendor" "$TARGET/symfony/var/cache" \ "$TARGET/symfony/var/log" "$TARGET/build" 2>/dev/null || true rm -f "$TARGET/symfony/composer.lock" \ "$TARGET/symfony/var/data.sqlite"* 2>/dev/null || true fi # ── Compute name variants used in templates ────────────────────────── # snake_lower (matches CMake project naming convention) and PascalCase # (matches QML module URI). Hyphens collapse to underscore for snake. SNAKE="$(printf '%s' "$NAME" | tr 'A-Z-' 'a-z_')" PASCAL="$(printf '%s' "$NAME" | awk -F'[-_]' '{ out="" for (i=1; i<=NF; i++) { s = $i out = out toupper(substr(s,1,1)) tolower(substr(s,2)) } print out }')" # ── Rewrite identifiers ────────────────────────────────────────────── say "rewriting identifiers (snake=$SNAKE, pascal=$PASCAL)" # qml/CMakeLists.txt: project(php_qml_skeleton) → project($SNAKE), every # `skeleton` Qt-target reference → $NAME, URI Skeleton → $PASCAL. sed -i \ -e "s/project(php_qml_skeleton/project($SNAKE/g" \ -e "s/qt_add_executable(skeleton /qt_add_executable($NAME /g" \ -e "s/qt_add_qml_module(skeleton/qt_add_qml_module($NAME/g" \ -e "s/target_link_libraries(skeleton /target_link_libraries($NAME /g" \ -e "s/URI Skeleton/URI $PASCAL/g" \ "$TARGET/qml/CMakeLists.txt" # qml/main.cpp: setApplicationName, SingleInstance lock, loadFromModule. sed -i \ -e "s|QStringLiteral(\"skeleton\")|QStringLiteral(\"$NAME\")|g" \ -e "s|loadFromModule(\"Skeleton\",|loadFromModule(\"$PASCAL\",|g" \ "$TARGET/qml/main.cpp" # qml/Main.qml: window title cosmetic. sed -i \ -e "s|php-qml — skeleton|php-qml — $NAME|g" \ "$TARGET/qml/Main.qml" # Makefile: rewrite identifiers for the appimage target — binary name, # packaging filenames, AppImage output filename. The path-repo + packaging # absolute paths are handled later (after we know vendor vs absolute mode). sed -i \ -e "s|\$(BUILD_DIR)/skeleton|\$(BUILD_DIR)/$NAME|g" \ -e "s|--app-name skeleton|--app-name $NAME|g" \ -e "s|packaging/skeleton.desktop|packaging/$NAME.desktop|g" \ -e "s|packaging/skeleton.png|packaging/$NAME.png|g" \ -e "s|build/Skeleton-x86_64.AppImage|build/$PASCAL-x86_64.AppImage|g" \ "$TARGET/Makefile" # Rename packaging files to match the app name + rewrite the Exec/Icon # fields in the .desktop file (XDG-launched binary lookup uses these). if [ -f "$TARGET/packaging/skeleton.desktop" ]; then mv "$TARGET/packaging/skeleton.desktop" "$TARGET/packaging/$NAME.desktop" sed -i \ -e "s|^Name=php-qml Skeleton|Name=php-qml $PASCAL|" \ -e "s|^Exec=skeleton|Exec=$NAME|" \ -e "s|^Icon=skeleton|Icon=$NAME|" \ "$TARGET/packaging/$NAME.desktop" fi if [ -f "$TARGET/packaging/skeleton.png" ]; then mv "$TARGET/packaging/skeleton.png" "$TARGET/packaging/$NAME.png" fi # .vscode/launch.json: binary path + config label both mention `skeleton`. if [ -f "$TARGET/.vscode/launch.json" ]; then sed -i \ -e "s|build/qml/skeleton|build/qml/$NAME|g" \ -e "s|Run skeleton (Qt host)|Run $NAME (Qt host)|g" \ "$TARGET/.vscode/launch.json" fi # ── Path-repo: absolute reference, or vendor a copy ────────────────── COMPOSER_JSON="$TARGET/symfony/composer.json" [ -f "$COMPOSER_JSON" ] || die "skeleton missing symfony/composer.json (corrupt copy?)" if [ "$VENDOR" -eq 1 ]; then say "vendoring framework/php → $NAME/.bridge/" mkdir -p "$TARGET/.bridge" say "vendoring framework/qml → $NAME/.bridge-qml/" mkdir -p "$TARGET/.bridge-qml" say "vendoring framework/packaging → $NAME/.bridge-packaging/" mkdir -p "$TARGET/.bridge-packaging" if command -v rsync >/dev/null 2>&1; then rsync -a --delete \ --exclude 'vendor/' --exclude '.phpunit.cache/' \ --exclude '.php-cs-fixer.cache' \ "$FRAMEWORK/framework/php/" "$TARGET/.bridge/" rsync -a --delete \ --exclude 'build/' \ "$FRAMEWORK/framework/qml/" "$TARGET/.bridge-qml/" rsync -a --delete \ "$FRAMEWORK/packaging/linux/" "$TARGET/.bridge-packaging/" else cp -R "$FRAMEWORK/framework/php/." "$TARGET/.bridge/" cp -R "$FRAMEWORK/framework/qml/." "$TARGET/.bridge-qml/" cp -R "$FRAMEWORK/packaging/linux/." "$TARGET/.bridge-packaging/" rm -rf "$TARGET/.bridge/vendor" "$TARGET/.bridge-qml/build" 2>/dev/null || true fi BUNDLE_URL="../.bridge" # qml/CMakeLists.txt lives at $TARGET/qml/, vendored qml module at # $TARGET/.bridge-qml/, so the relative path from the consumer is ../.bridge-qml. QML_FW_PATH="\${CMAKE_CURRENT_SOURCE_DIR}/../.bridge-qml" PACKAGING_PATH=".bridge-packaging" else BUNDLE_URL="$FRAMEWORK/framework/php" QML_FW_PATH="$FRAMEWORK/framework/qml" PACKAGING_PATH="$FRAMEWORK/packaging/linux" fi say "path-repo → $BUNDLE_URL" # Replace the original "../../php" path-repo URL. The skeleton's # composer.json is the source of truth here, so the literal is stable. sed -i \ -e "s|\"../../php\"|\"$BUNDLE_URL\"|g" \ "$COMPOSER_JSON" # Rewrite CMake's add_subdirectory(.../../qml) — same problem: skeleton's # original relative path assumed the qml module sat alongside it inside # the framework checkout, which isn't true once scaffolded out. say "qml framework path → $QML_FW_PATH" # Use a sed delimiter that can't appear in a path. Pipe is fine here # because we don't allow pipes in $FRAMEWORK (it'd break a lot earlier). sed -i \ -e "s|\${CMAKE_CURRENT_SOURCE_DIR}/../../qml|$QML_FW_PATH|g" \ "$TARGET/qml/CMakeLists.txt" # Makefile: BUNDLE_SRC + PACKAGING were framework-tree relative; rewrite # to absolute (or the vendored path). Both are matched against the literal # values in the skeleton Makefile. say "appimage paths → bundle=$BUNDLE_URL packaging=$PACKAGING_PATH" sed -i \ -e "s|^BUNDLE_SRC := ../../php\$|BUNDLE_SRC := $BUNDLE_URL|" \ -e "s|^PACKAGING := ../../packaging/linux\$|PACKAGING := $PACKAGING_PATH|" \ "$TARGET/Makefile" # ── Composer install + first-run migrations ────────────────────────── if [ "$SKIP_INSTALL" -eq 1 ]; then say "skipping composer install (--skip-install)" else command -v composer >/dev/null 2>&1 || die "composer not on PATH" say "composer install" (cd "$TARGET/symfony" && composer install --no-interaction) if [ -x "$TARGET/symfony/bin/console" ]; then say "first-run migrations" (cd "$TARGET/symfony" \ && bin/console doctrine:migrations:migrate -n 2>/dev/null \ || warn "migrations skipped (no migrations yet, or DB not ready)") fi fi # ── Optional: git init so newcomers get a clean baseline ───────────── if [ "$GIT_INIT" -eq 1 ]; then if command -v git >/dev/null 2>&1; then say "git init" ( cd "$TARGET" git init -q git add -A git commit -q -m "Initial scaffold from php-qml-init" || \ warn "git commit failed (continuing)" ) else warn "git not on PATH, skipping --git" fi fi # ── Next steps ─────────────────────────────────────────────────────── cat <