diff --git a/bin/php-qml-init b/bin/php-qml-init new file mode 100755 index 0000000..21cdb1c --- /dev/null +++ b/bin/php-qml-init @@ -0,0 +1,245 @@ +#!/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: $(BUILD_DIR)/skeleton → $(BUILD_DIR)/$NAME. +sed -i \ + -e "s|\$(BUILD_DIR)/skeleton|\$(BUILD_DIR)/$NAME|g" \ + "$TARGET/Makefile" + +# ── 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" + 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/" + else + cp -R "$FRAMEWORK/framework/php/." "$TARGET/.bridge/" + cp -R "$FRAMEWORK/framework/qml/." "$TARGET/.bridge-qml/" + 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" +else + BUNDLE_URL="$FRAMEWORK/framework/php" + QML_FW_PATH="$FRAMEWORK/framework/qml" +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" + +# ── 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 <