diff --git a/scripts/android/download_libs.sh b/scripts/android/download_libs.sh index 27580d660fa..a27c27501d2 100755 --- a/scripts/android/download_libs.sh +++ b/scripts/android/download_libs.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Platform-clean removes only lib/android; other platforms stay installed. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR -../dev/download_libs.sh -p android -b $@ +cd "$SCRIPT_DIR" +../dev/download_libs.sh -p android -b "$@" diff --git a/scripts/dev/download_libs.sh b/scripts/dev/download_libs.sh index e932f55e377..495024a843f 100755 --- a/scripts/dev/download_libs.sh +++ b/scripts/dev/download_libs.sh @@ -4,12 +4,23 @@ VER=latest PLATFORM="" ARCH="" OVERWRITE=1 +FULL_CLEAN=0 +# SHA: VERIFY_SHA=1 (default) check GitHub release asset digests when present +# VERIFY_SHA=0 (--skip-sha) never verify +# REQUIRE_SHA=1 (--require-sha) fail if digest missing or tools unavailable +VERIFY_SHA=1 +REQUIRE_SHA=0 LEGACY=0 SILENT_ARGS="" NO_SSL="" BLEEDING_EDGE=0 -DL_VERSION=2.7.1 +DL_VERSION=2.7.4 TAG="" +REPO="latest" +SHA_VERIFIED=0 +SHA_SKIPPED=0 +SHA_FAILED=0 +SHA_STATUS="unchecked" printHelp(){ cat << EOF @@ -25,8 +36,14 @@ cat << EOF msys2: 64 android: armv7, arm64, and x86 (if not specified will download all) linux: 64gcc6, armv6l or armv7l - -n, --no-overwrite Merge new libraries with existing ones, use only to download same version for different platforms - If not set deletes any existing libraries + -n, --no-overwrite Pure merge: do not delete anything before extract. + Default (without -n) only removes libs//lib/\$PLATFORM so other + platforms (android + ios + macos + emscripten …) can coexist. + --full-clean Also remove shared include/ (and bin/ for vs|msys2) for libs being + installed. Use for a hard reset of one platform install; not needed + for multi-platform side-by-side installs. + --skip-sha Do not verify package SHA-256 against GitHub release digests + --require-sha Fail if a package has no digest or local hash tools are missing -s, --silent Silent download progress -h, --help Shows this message -k, --no-ssl Allow no SSL validation @@ -38,26 +55,50 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" if [[ ! -d "$SCRIPT_DIR" ]]; then SCRIPT_DIR="$PWD"; fi . "$SCRIPT_DIR/downloader.sh" -download(){ - echo ' -----' - #echo " Downloading $1" - # downloader ci.openframeworks.cc/libs/$1 $SILENT_ARGS - - COMMAND=" " - +# Resolve apothecary release tag used for package URLs +resolveRepo(){ if [[ $BLEEDING_EDGE = 1 ]] ; then REPO="latest" else REPO="nightly" fi - #FIXME: remove later, now forcing "latest" REPO="latest" - - if [[ $TAG != "" ]] ; then REPO="$TAG" fi +} + +# Shared SHA-256 helpers (GitHub digests + sidecars) +. "$SCRIPT_DIR/sha_verify.sh" + +# Apothecary-specific: map REPO tag → digest map, then verify packages in cwd +fetchReleaseDigests(){ + local tag="$1" + local out="$2" + fetchGithubReleaseDigests "openframeworks/apothecary" "$tag" "$out" +} + +verifyPackageDigests(){ + local pkgs="$1" + local map + map=$(mktemp 2>/dev/null || mktemp -t ofdigestmap) + if ! fetchReleaseDigests "$REPO" "$map"; then + : > "$map" + fi + verifyPackagesAgainstMap "$pkgs" "$map" "GitHub apothecary release digests" + local rc=$? + rm -f "$map" + return $rc +} + +download(){ + echo ' -----' + #echo " Downloading $1" + # downloader ci.openframeworks.cc/libs/$1 $SILENT_ARGS + + COMMAND=" " + resolveRepo for PKG in $1; do COMMAND+="https://github.com/openframeworks/apothecary/releases/download/$REPO/$PKG " @@ -107,6 +148,17 @@ while [[ $# -gt 0 ]]; do -n|--no-overwrite) OVERWRITE=0 ;; + --full-clean) + FULL_CLEAN=1 + OVERWRITE=1 + ;; + --skip-sha) + VERIFY_SHA=0 + ;; + --require-sha) + REQUIRE_SHA=1 + VERIFY_SHA=1 + ;; -b|--bleeding-edge) BLEEDING_EDGE=1 ;; @@ -161,6 +213,32 @@ if [ "$PLATFORM" == "" ]; then fi fi +# Visual Studio package arch for this machine: 64 | arm64 +# (arm64 host also pulls arm64ec — see VS package selection below) +detectHostVsArch(){ + local m pa + # Windows env (Git Bash / MSYS often set these) + pa="${PROCESSOR_ARCHITECTURE:-}" + [[ -n "${PROCESSOR_ARCHITEW6432:-}" ]] && pa="${PROCESSOR_ARCHITEW6432}" + case "${MSYSTEM:-}" in + CLANGARM64|clangarm64) echo "arm64"; return 0 ;; + esac + case "$(echo "$pa" | tr '[:lower:]' '[:upper:]')" in + ARM64) echo "arm64"; return 0 ;; + AMD64|X86) echo "64"; return 0 ;; + esac + m=$(uname -m 2>/dev/null || echo "") + case "$m" in + aarch64|arm64|ARM64) echo "arm64"; return 0 ;; + x86_64|amd64|i686|i386|x86) echo "64"; return 0 ;; + esac + # uname -s might be MINGW64_NT-… on x64 + case "$(uname -s 2>/dev/null)" in + *ARM64*|*arm64*) echo "arm64"; return 0 ;; + esac + echo "64" +} + if [ "$ARCH" == "" ]; then if [ "$PLATFORM" == "linux" ]; then ARCH=$(uname -m) @@ -203,6 +281,10 @@ EOF else ARCH=clang64 fi + elif [ "$PLATFORM" == "vs" ]; then + # Default: host arch only (not every VS package). Use -a all for 64+arm64+arm64ec. + ARCH=$(detectHostVsArch) + echo " VS host arch → ${ARCH} (pass -a all for every VS arch, or -a 64|arm64|arm64ec)" fi if [ "$PLATFORM" == "osx" ]; then @@ -210,6 +292,16 @@ EOF fi fi +# Normalize aliases for VS package names +if [ "$PLATFORM" == "vs" ]; then + case "$ARCH" in + x64|x86_64|amd64|win64) ARCH=64 ;; + aarch64|ARM64|clangarm64) ARCH=arm64 ;; + all|multi|host-all) ARCH=all ;; + host) ARCH=$(detectHostVsArch) ;; + esac +fi + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$SCRIPT_DIR" @@ -246,50 +338,51 @@ if [ "$PLATFORM" == "msys2" ]; then else PKGS="openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}.zip" fi -elif [ "$ARCH" == "" ] && [ "$PLATFORM" == "vs" ]; then +elif [ "$PLATFORM" == "vs" ]; then + if [[ $VS_2026 == 1 ]]; then + VS_PLATFORM="${PLATFORM}_2026" + else + VS_PLATFORM="${PLATFORM}" + fi if [[ $BLEEDING_EDGE = 1 ]]; then if [[ $LEGACY == 1 ]]; then PKGS="openFrameworksLibs_${VER}_${PLATFORM}_2019_64_1.zip \ openFrameworksLibs_${VER}_${PLATFORM}_2019_64_2.zip" + elif [[ "$ARCH" == "all" ]]; then + # Explicit multi-arch (CI / packagers) + PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_64_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_64_2.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_2.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_2.zip" + elif [[ "$ARCH" == "arm64" ]]; then + # Windows on ARM: native arm64 + arm64ec (x64-compat) for Win11 ARM + echo " VS packages: arm64 + arm64ec (Windows on ARM host)" + PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_2.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_2.zip" + elif [[ "$ARCH" == "arm64ec" ]]; then + PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_2.zip" else - if [[ $VS_2026 == 1 ]]; then - VS_PLATFORM="${PLATFORM}_2026" - else - VS_PLATFORM="${PLATFORM}" - fi - PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_64_1.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_64_2.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_1.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64_2.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_1.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_arm64ec_2.zip" - + # x64 host (default) + PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_64_1.zip \ + openFrameworksLibs_${VER}_${VS_PLATFORM}_64_2.zip" fi else - PKGS="openFrameworksLibs_${VER}_${PLATFORM}_64_1.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_64_2.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_64_3.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_64_4.zip" - fi -elif [ "$PLATFORM" == "vs" ]; then - if [[ $BLEEDING_EDGE = 1 ]] ; then - if [[ $VS_2026 == 1 ]]; then - VS_PLATFORM="${PLATFORM}_2026" - else - VS_PLATFORM="${PLATFORM}" - fi - if [[ $LEGACY == 1 ]]; then - PKGS="openFrameworksLibs_${VER}_${PLATFORM}_2019_64_1.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_2019_64_2.zip" + if [[ "$ARCH" == "all" ]]; then + PKGS="openFrameworksLibs_${VER}_${PLATFORM}_64_1.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_64_2.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_64_3.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_64_4.zip" else - PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_${ARCH}_1.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_${ARCH}_2.zip" + PKGS="openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_1.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_2.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_3.zip \ + openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_4.zip" fi - else - PKGS="openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_1.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_2.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_3.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_${ARCH}_4.zip" fi elif [[ "$PLATFORM" =~ ^(osx|ios|tvos|xros|catos|watchos|macos)$ ]]; then if [[ $BLEEDING_EDGE = 1 ]] ; then @@ -348,33 +441,58 @@ cd download download "${PKGS[@]}" +# Integrity: GitHub release asset digests (sha256:…) when published +if ! verifyPackageDigests "$PKGS"; then + writeVerifyState "$(pwd)" "$REPO" "$PLATFORM" "$ARCH" + exit 1 +fi +writeVerifyState "$(pwd)" "$REPO" "$PLATFORM" "$ARCH" + cd ../ # back to libs libs=("cairo" "curl" "FreeImage" "brotli" "fmod" "freetype" "glew" "glfw" "json" "libpng" "openssl" "pixman" "poco" "rtAudio" "tess2" "uriparser" "utf8" "videoInput" "zlib" "opencv" "ippicv" "assimp" "libxml2" "svgtiny" "fmt") + +# Resolve which lib/ folder this package uses. +# Apple multi-target packages (macos, and ios/tvos/… wrappers that pass -p macos) +# install under lib/macos/*.xcframework. Plain "osx" may use lib/osx and/or a +# macos-arm64_x86_64 slice — never wipe the whole lib/macos tree for -p osx. +LIB_PLATFORM_DIR="$PLATFORM" +case "$PLATFORM" in + ios|tvos|xros|catos|watchos|macos) LIB_PLATFORM_DIR="macos" ;; +esac + if [ $OVERWRITE -eq 1 ]; then echo " " - echo " Overwrite - Removing prior libraries for [$PLATFORM]" + if [ $FULL_CLEAN -eq 1 ]; then + echo " Full-clean - Removing platform libs + shared include/bin for [$PLATFORM]" + else + echo " Platform-clean - Removing prior libs only under lib/[$LIB_PLATFORM_DIR] (other platforms kept)" + fi for ((i=0;i<${#libs[@]};++i)); do - if [ -e "${libs[i]}/lib/$PLATFORM" ]; then - echo " Removing: [${libs[i]}/lib/$PLATFORM]" - rm -rf "${libs[i]}/lib/$PLATFORM" + if [ -e "${libs[i]}/lib/$LIB_PLATFORM_DIR" ]; then + echo " Removing: [${libs[i]}/lib/$LIB_PLATFORM_DIR]" + rm -rf "${libs[i]}/lib/$LIB_PLATFORM_DIR" fi - if [ "$PLATFORM" == "msys2" ] || [ "$PLATFORM" == "vs" ]; then - if [ -e "${libs[i]}/bin" ]; then - echo " Removing: [${libs[i]}/bin]" - rm -rf "${libs[i]}/bin" + # Shared include/ + vs|msys2 bin/ only with --full-clean (not for multi-platform installs) + if [ $FULL_CLEAN -eq 1 ]; then + if [ "$PLATFORM" == "msys2" ] || [ "$PLATFORM" == "vs" ]; then + if [ -e "${libs[i]}/bin" ]; then + echo " Removing: [${libs[i]}/bin]" + rm -rf "${libs[i]}/bin" + fi + fi + if [ -e "${libs[i]}/include" ]; then + echo " Removing: [${libs[i]}/include]" + rm -rf "${libs[i]}/include" fi fi - if [ -e "${libs[i]}/include" ]; then - echo " Removing: [${libs[i]}/include]" - rm -rf "${libs[i]}/include" - fi - done fi +# osx host packages may also refresh the desktop slice inside lib/macos/*.xcframework +# without deleting ios/tvos/… sibling slices in the same xcframework. if [ "$PLATFORM" == "osx" ]; then echo " " - echo " Overwrite - Removing prior libraries for [$PLATFORM]" + echo " xcframework - refresh macos host slice (ios/tvos/… slices kept)" for ((i=0;i<${#libs[@]};++i)); do xcframework_path="${libs[i]}/lib/macos/${libs[i]}.xcframework/macos-arm64_x86_64" if [ $OVERWRITE -eq 1 ]; then @@ -385,7 +503,16 @@ if [ "$PLATFORM" == "osx" ]; then fi info_plist_path="${libs[i]}/lib/macos/${libs[i]}.xcframework/Info.plist" if [ -e "$info_plist_path" ]; then - #echo " Backing up: [${info_plist_path}] to [${info_plist_path}.bak]" + cp "$info_plist_path" "${info_plist_path}.bak" + fi + done +elif [ "$PLATFORM" == "macos" ]; then + # Full lib/macos already cleared above when OVERWRITE=1; only need Info.plist backup if merge + echo " " + echo " xcframework - macos multi-target package (lib/macos)" + for ((i=0;i<${#libs[@]};++i)); do + info_plist_path="${libs[i]}/lib/macos/${libs[i]}.xcframework/Info.plist" + if [ -e "$info_plist_path" ]; then cp "$info_plist_path" "${info_plist_path}.bak" fi done @@ -457,36 +584,47 @@ fi echo " ------ " if [ "$PLATFORM" == "osx" ]; then if [ $OVERWRITE -eq 1 ]; then - echo " Overwrite - addon xCFramework: [${addons[i]} - ${addonslibs[i]}]" - xcframework_path="../addons/${addons[i]}/libs/${addonslibs[i]}/lib/macos/${addonslibs[i]}.xcframework/macos-arm64_x86_64" - if [ -e "$xcframework_path" ]; then - echo " Removing: [$xcframework_path]" - rm -rf "$xcframework_path" - fi - fi - info_plist_path="../addons/${addons[i]}/libs/${addonslibs[i]}/lib/macos/${addonslibs[i]}.xcframework/Info.plist" - if [ -e "$info_plist_path" ]; then - cp "$info_plist_path" "${info_plist_path}.bak" + # Refresh desktop slice only — keep ios/tvos/… slices in the same xcframework + for ((i=0;i<${#addonslibs[@]};++i)); do + xcframework_path="../addons/${addons[i]}/libs/${addonslibs[i]}/lib/macos/${addonslibs[i]}.xcframework/macos-arm64_x86_64" + if [ -e "$xcframework_path" ]; then + echo " Removing addon host slice: [$xcframework_path]" + rm -rf "$xcframework_path" + fi + info_plist_path="../addons/${addons[i]}/libs/${addonslibs[i]}/lib/macos/${addonslibs[i]}.xcframework/Info.plist" + if [ -e "$info_plist_path" ]; then + cp "$info_plist_path" "${info_plist_path}.bak" + fi + done fi +elif [ "$PLATFORM" == "macos" ]; then + for ((i=0;i<${#addonslibs[@]};++i)); do + info_plist_path="../addons/${addons[i]}/libs/${addonslibs[i]}/lib/macos/${addonslibs[i]}.xcframework/Info.plist" + if [ -e "$info_plist_path" ]; then + cp "$info_plist_path" "${info_plist_path}.bak" + fi + done fi -if [ $OVERWRITE -eq 1 ]; then +if [ $OVERWRITE -eq 1 ]; then for ((i=0;i<${#addonslibs[@]};++i)); do if [ -e ${addonslibs[i]} ] ; then - echo " Overwrite - addon: [${addons[i]} - ${addonslibs[i]}]" - if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/lib/$PLATFORM ]; then - echo " Remove binaries: [${addons[i]}/libs/${addonslibs[i]}/lib/$PLATFORM]" - rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/lib/$PLATFORM - fi - if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/bin ]; then - echo " Remove binaries: [${addons[i]}/libs/${addonslibs[i]}/bin]" - rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/bin + echo " Platform-clean - addon: [${addons[i]} - ${addonslibs[i]}] → lib/$LIB_PLATFORM_DIR" + if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/lib/$LIB_PLATFORM_DIR ]; then + echo " Remove binaries: [${addons[i]}/libs/${addonslibs[i]}/lib/$LIB_PLATFORM_DIR]" + rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/lib/$LIB_PLATFORM_DIR fi - if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/include ]; then - echo " Remove include: [${addons[i]}/libs/include]" - rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/include + if [ $FULL_CLEAN -eq 1 ]; then + if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/bin ]; then + echo " Remove binaries: [${addons[i]}/libs/${addonslibs[i]}/bin]" + rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/bin + fi + if [ -e ../addons/${addons[i]}/libs/${addonslibs[i]}/include ]; then + echo " Remove include: [${addons[i]}/libs/${addonslibs[i]}/include]" + rm -rf ../addons/${addons[i]}/libs/${addonslibs[i]}/include + fi fi fi done @@ -520,4 +658,10 @@ if [ "$PLATFORM" == "osx" ]; then fi echo " ------ " -echo " openFrameworks download_libs and install complete!" +if [[ "$SHA_STATUS" == "verified" ]]; then + echo " openFrameworks download_libs complete — secure (SHA-256 verified: ${SHA_VERIFIED} package(s))" +elif [[ "$SHA_STATUS" == "skipped" || "$SHA_STATUS" == "no-digest" || "$SHA_STATUS" == "no-remote" || "$SHA_STATUS" == "no-tools" ]]; then + echo " openFrameworks download_libs complete — integrity not fully verified (${SHA_STATUS})" +else + echo " openFrameworks download_libs and install complete!" +fi diff --git a/scripts/dev/lib_sources.sh b/scripts/dev/lib_sources.sh index 1da37a71a53..4079adc20bb 100755 --- a/scripts/dev/lib_sources.sh +++ b/scripts/dev/lib_sources.sh @@ -13,6 +13,11 @@ OFLIBS_REPO="${OFLIBS_REPO:-ofWorks/ofLibs}" ARCHIVE_BASE="${ARCHIVE_BASE:-https://libs.danoli3.com}" APOTHECARY_RELEASES="${APOTHECARY_RELEASES:-https://github.com/openframeworks/apothecary/releases/download}" +# Shared SHA-256 helpers (GitHub digests + URL sidecars) +_OF_SHA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=sha_verify.sh +. "${_OF_SHA_DIR}/sha_verify.sh" + # ofLibs platform token for a host OF platform ofLibsPlatform(){ local p="${1:-osx}" @@ -133,6 +138,12 @@ downloadApothecaryLibs(){ else args+=( -b ) fi + # LIB_CLEAN_MODE: platform (default) | merge (-n) | full (--full-clean) + case "${LIB_CLEAN_MODE:-platform}" in + merge|no-overwrite|n) args+=( -n ) ;; + full|full-clean) args+=( --full-clean ) ;; + platform|*) ;; # default platform-scoped clean in download_libs.sh + esac "$script" "${args[@]}" } @@ -144,8 +155,9 @@ downloadOfLibs(){ local ofPlat="${3:-osx}" shift 3 || true local -a libs=("$@") - local plat pkg url tmp dest name ofName ok=0 skip=0 + local plat pkg url tmp dest name ofName ok=0 skip=0 fail=0 local dlDir="${ofDir}/libs/download/oflibs" + local digestMap expected plat=$(ofLibsPlatform "$ofPlat") if [[ -z "$plat" ]]; then @@ -171,6 +183,21 @@ downloadOfLibs(){ # shellcheck disable=SC2064 trap "rm -rf '$tmp'" RETURN + # Prefetch GitHub release digests once (ofLibs publishes digest on every asset) + digestMap=$(mktemp 2>/dev/null || mktemp -t oflibsdigest) + if [[ "${VERIFY_SHA:-1}" != "0" ]]; then + echo " ofLibs fetching SHA-256 digests for ${OFLIBS_REPO}@${tag}…" + if ! fetchGithubReleaseDigests "$OFLIBS_REPO" "$tag" "$digestMap"; then + echo " ! could not fetch ofLibs digests — packages will install without verify" >&2 + : > "$digestMap" + fi + fi + + SHA_VERIFIED=0 + SHA_SKIPPED=0 + SHA_FAILED=0 + SHA_STATUS="unchecked" + echo " ofLibs tag=${tag} platform=${plat} libs=${#libs[@]} → ${ofDir}/libs" for name in "${libs[@]}"; do [[ -n "$name" ]] || continue @@ -185,23 +212,78 @@ downloadOfLibs(){ skip=$((skip + 1)) continue fi + + # SHA-256 against GitHub release asset digest when available + if [[ "${VERIFY_SHA:-1}" != "0" ]]; then + if ! sha_has_tools; then + SHA_STATUS="no-tools" + [[ "${REQUIRE_SHA:-0}" == "1" ]] && { rm -f "$digestMap"; return 1; } + else + expected=$(lookupDigest "$digestMap" "$pkg") + if [[ -z "$expected" ]]; then + SHA_SKIPPED=$((SHA_SKIPPED+1)) + echo " ~ no digest for ${pkg} — skip verify" + elif verifyFileSha256 "${dlDir}/${pkg}" "$expected"; then + SHA_VERIFIED=$((SHA_VERIFIED+1)) + echo " ✓ sha256 verified (secure)" + else + SHA_FAILED=$((SHA_FAILED+1)) + fail=$((fail + 1)) + echo " ✗ SHA-256 MISMATCH — removing ${pkg}" >&2 + rm -f "${dlDir}/${pkg}" "${dlDir}/${pkg}.sha256" + continue + fi + fi + fi + rm -rf "${tmp}/extract" mkdir -p "${tmp}/extract" "$dest" unzip -qo "${dlDir}/${pkg}" -d "${tmp}/extract" + # Merge into libs/ so other platforms under lib// are kept. + # Headers are shared and replaced; only this package's lib/* children are replaced. if [[ -d "${tmp}/extract/include" ]]; then - rm -rf "${dest}/include" - mv "${tmp}/extract/include" "${dest}/include" + mkdir -p "${dest}/include" + if command -v rsync &>/dev/null; then + rsync -a --delete "${tmp}/extract/include/" "${dest}/include/" + else + rm -rf "${dest}/include" + mv "${tmp}/extract/include" "${dest}/include" + fi fi if [[ -d "${tmp}/extract/lib" ]]; then - rm -rf "${dest}/lib" - mv "${tmp}/extract/lib" "${dest}/lib" + mkdir -p "${dest}/lib" + # Replace only platform subdirs present in the package (e.g. lib/macos, lib/android) + local child + for child in "${tmp}/extract/lib"/*; do + [[ -e "$child" ]] || continue + local base + base=$(basename "$child") + rm -rf "${dest}/lib/${base}" + mv "$child" "${dest}/lib/${base}" + done fi [[ -f "${tmp}/extract/copying.txt" ]] && cp "${tmp}/extract/copying.txt" "${dest}/" 2>/dev/null || true ok=$((ok + 1)) done - echo " ofLibs install complete ok=${ok} skipped=${skip} (libs//{include,lib})" + + if [[ $SHA_FAILED -gt 0 ]]; then + SHA_STATUS="failed" + elif [[ $SHA_VERIFIED -gt 0 ]]; then + SHA_STATUS="verified" + elif [[ $SHA_SKIPPED -gt 0 ]]; then + SHA_STATUS="no-digest" + elif [[ "${VERIFY_SHA:-1}" == "0" ]]; then + SHA_STATUS="skipped" + fi + writeVerifyState "${ofDir}/libs/download" "oflibs:${tag}" "$ofPlat" "" + rm -f "$digestMap" + + echo " ofLibs install complete ok=${ok} skipped=${skip} sha_fail=${fail} (libs//{include,lib/})" + if [[ $SHA_VERIFIED -gt 0 ]]; then + echo " ✓ Secure download: ${SHA_VERIFIED} ofLibs package(s) verified SHA-256 against GitHub digests" + fi echo " note: ofLibs is ofWorks-oriented; stock oF may need path tweaks for some libs." - [[ $ok -gt 0 ]] + [[ $fail -eq 0 && $ok -gt 0 ]] } # Download a historical full OF release package from danoli3 archive @@ -237,6 +319,44 @@ downloadArchiveRelease(){ return 1 fi + # SHA when published on libs.danoli3.com (sidecar .sha256 or SHA256SUMS — see generate_sha256.php) + SHA_VERIFIED=0 + SHA_SKIPPED=0 + SHA_FAILED=0 + SHA_STATUS="unchecked" + if [[ "${VERIFY_SHA:-1}" != "0" ]]; then + echo " archive checking SHA-256 sidecars…" + if ! sha_has_tools; then + SHA_STATUS="no-tools" + echo " ! no local sha256 tool — skip verify" + [[ "${REQUIRE_SHA:-0}" == "1" ]] && return 1 + else + local expected + expected=$(fetchUrlSidecarDigest "$url") || expected="" + if [[ -z "$expected" ]]; then + SHA_SKIPPED=1 + SHA_STATUS="no-digest" + echo " ~ no SHA published for ${pkg} yet (add ${pkg}.sha256 or SHA256SUMS on server)" + [[ "${REQUIRE_SHA:-0}" == "1" ]] && return 1 + elif verifyFileSha256 "${dlDir}/${pkg}" "$expected"; then + SHA_VERIFIED=1 + SHA_STATUS="verified" + echo " ✓ sha256 verified (secure)" + echo " sha256:${expected}" + else + SHA_FAILED=1 + SHA_STATUS="failed" + echo " ✗ SHA-256 MISMATCH — removing corrupt download" >&2 + rm -f "${dlDir}/${pkg}" "${dlDir}/${pkg}.sha256" + writeVerifyState "${ofDir}/libs/download" "archive:${ver}" "$ofPlat" "$arch" + return 1 + fi + fi + else + SHA_STATUS="skipped" + fi + writeVerifyState "${ofDir}/libs/download" "archive:${ver}" "$ofPlat" "$arch" + tmp=$(mktemp -d "${TMPDIR:-/tmp}/ofarchive.XXXXXX") # shellcheck disable=SC2064 trap "rm -rf '$tmp'" RETURN diff --git a/scripts/dev/sha_verify.sh b/scripts/dev/sha_verify.sh new file mode 100644 index 00000000000..6edbf6c6c39 --- /dev/null +++ b/scripts/dev/sha_verify.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# sha_verify.sh — shared SHA-256 helpers for download_libs / ofLibs / archive +# Source from other scripts: . "$(dirname "$0")/sha_verify.sh" +# +# Env: +# VERIFY_SHA=1|0 default 1 +# REQUIRE_SHA=1|0 default 0 — fail when digest missing / no tools + +VERIFY_SHA="${VERIFY_SHA:-1}" +REQUIRE_SHA="${REQUIRE_SHA:-0}" +SHA_VERIFIED="${SHA_VERIFIED:-0}" +SHA_SKIPPED="${SHA_SKIPPED:-0}" +SHA_FAILED="${SHA_FAILED:-0}" +SHA_STATUS="${SHA_STATUS:-unchecked}" + +# Cross-platform SHA-256 of a file → lowercase hex (no filename) +sha256_file(){ + local f="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$f" 2>/dev/null | awk '{print tolower($1)}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$f" 2>/dev/null | awk '{print tolower($1)}' + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$f" 2>/dev/null | awk '{print tolower($NF)}' + else + return 1 + fi +} + +sha_has_tools(){ + command -v sha256sum >/dev/null 2>&1 \ + || command -v shasum >/dev/null 2>&1 \ + || command -v openssl >/dev/null 2>&1 +} + +# Parse GitHub Releases JSON → namehex (uses asset "digest": "sha256:…") +# Usage: githubReleaseDigestsToMap +githubReleaseDigestsToMap(){ + local api="$1" + local out="$2" + : > "$out" + [[ -f "$api" ]] || return 1 + if command -v python3 >/dev/null 2>&1; then + python3 - "$api" "$out" <<'PY' +import json, sys +src, dst = sys.argv[1], sys.argv[2] +with open(src, encoding="utf-8") as f: + data = json.load(f) +with open(dst, "w", encoding="utf-8") as out: + for a in data.get("assets") or []: + name = a.get("name") or "" + digest = a.get("digest") or "" + if not name or not digest: + continue + if ":" in digest: + algo, hx = digest.split(":", 1) + else: + algo, hx = "sha256", digest + if algo.lower() != "sha256": + continue + out.write(f"{name}\t{hx.lower()}\n") +PY + else + awk ' + /"name":/ { + if (match($0, /"name":[[:space:]]*"[^"]+"/)) { + n = substr($0, RSTART, RLENGTH) + gsub(/"name":[[:space:]]*"/, "", n) + gsub(/"/, "", n) + name = n + } + } + /"digest":/ { + if (name != "" && match($0, /sha256:[a-fA-F0-9]+/)) { + d = substr($0, RSTART, RLENGTH) + sub(/^sha256:/, "", d) + print name "\t" tolower(d) + name = "" + } + } + ' "$api" > "$out" + fi + [[ -s "$out" ]] +} + +# Fetch digests for owner/repo @ tag (or "latest") into map file +# Usage: fetchGithubReleaseDigests owner/repo tag out.map +fetchGithubReleaseDigests(){ + local repo="$1" + local tag="$2" + local out="$3" + local url api + : > "$out" + if [[ "$tag" == "latest" ]]; then + url="https://api.github.com/repos/${repo}/releases/latest" + else + url="https://api.github.com/repos/${repo}/releases/tags/${tag}" + fi + api=$(mktemp 2>/dev/null || mktemp -t ofdigest) + if ! curl -fsSL -H "Accept: application/vnd.github+json" \ + -H "User-Agent: openFrameworks-sha-verify" \ + "$url" -o "$api" 2>/dev/null; then + rm -f "$api" + return 1 + fi + githubReleaseDigestsToMap "$api" "$out" + local rc=$? + rm -f "$api" + return $rc +} + +lookupDigest(){ + local map="$1" + local name="$2" + [[ -f "$map" ]] || return 1 + awk -F'\t' -v n="$name" '$1==n {print $2; exit}' "$map" +} + +# Parse common checksum file formats → hex only for basename +# Accepts: +# deadbeef... +# deadbeef filename +# deadbeef *filename +# sha256:deadbeef +parseChecksumText(){ + local text="$1" + local want="$2" # optional basename to match + local line hx file + # Store regex in vars so bash 3.2 does not mangle {64} via brace expansion + local re_sha='^sha256:([a-fA-F0-9]{64})$' + local re_hex='^([a-fA-F0-9]{64})$' + local re_sum='^([a-fA-F0-9]{64})[[:space:]]+\*?(.+)$' + while IFS= read -r line || [[ -n "$line" ]]; do + line=$(printf '%s' "$line" | tr -d '\r') + [[ -z "$line" || "$line" == \#* ]] && continue + # sha256:HEX + if [[ "$line" =~ $re_sha ]]; then + printf '%s' "$(printf '%s' "${BASH_REMATCH[1]}" | tr 'A-F' 'a-f')" + return 0 + fi + # HEX only + if [[ "$line" =~ $re_hex ]]; then + printf '%s' "$(printf '%s' "${BASH_REMATCH[1]}" | tr 'A-F' 'a-f')" + return 0 + fi + # HEX file or HEX *file + if [[ "$line" =~ $re_sum ]]; then + hx=$(printf '%s' "${BASH_REMATCH[1]}" | tr 'A-F' 'a-f') + file=$(basename "${BASH_REMATCH[2]}") + if [[ -z "$want" || "$file" == "$want" ]]; then + printf '%s' "$hx" + return 0 + fi + fi + done <<< "$text" + return 1 +} + +# Fetch expected digest for a remote package URL (sidecar / SHA256SUMS) +# Tries: URL.sha256 URL.sha256sum dir/SHA256SUMS dir/checksums.sha256 dir/sha256sums.txt +fetchUrlSidecarDigest(){ + local url="$1" + local basename + basename=$(basename "$url") + local dir text + dir=$(printf '%s' "$url" | sed 's|/[^/]*$||') + local candidates=( + "${url}.sha256" + "${url}.sha256sum" + "${dir}/${basename}.sha256" + "${dir}/SHA256SUMS" + "${dir}/sha256sums.txt" + "${dir}/checksums.sha256" + "${dir}/checksums.txt" + ) + local c + for c in "${candidates[@]}"; do + text=$(curl -fsSL --max-time 15 "$c" 2>/dev/null) || continue + [[ -z "$text" ]] && continue + if parseChecksumText "$text" "$basename"; then + return 0 + fi + done + return 1 +} + +# Verify one local file against expected hex. Writes sidecar .sha256 on success. +# Sets: returns 0 match, 1 mismatch, 2 no expected / skip +verifyFileSha256(){ + local file="$1" + local expected="$2" + local actual + [[ -f "$file" ]] || return 1 + if [[ -z "$expected" ]]; then + return 2 + fi + expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f' | tr -d ' \r\n') + actual=$(sha256_file "$file") || actual="" + if [[ -z "$actual" ]]; then + return 1 + fi + if [[ "$actual" == "$expected" ]]; then + printf '%s\n' "$actual" > "${file}.sha256" + return 0 + fi + return 1 +} + +writeVerifyState(){ + local state_dir="$1" + local release="${2:-}" + local platform="${3:-}" + local arch="${4:-}" + mkdir -p "$state_dir" + cat > "${state_dir}/.last-verify" << EOF +status=${SHA_STATUS} +algo=sha256 +release=${release} +platform=${platform} +arch=${arch} +verified=${SHA_VERIFIED} +skipped=${SHA_SKIPPED} +failed=${SHA_FAILED} +updated=$(date -u +%Y-%m-%dT%H:%M:%SZ) +EOF +} + +# Verify list of basenames in cwd against a digest map file (namehex) +# Usage: verifyPackagesAgainstMap "pkg1 pkg2" mapfile +verifyPackagesAgainstMap(){ + local pkgs="$1" + local map="$2" + local label="${3:-GitHub release digests}" + local expected actual pkg any_expected=0 + + SHA_VERIFIED=0 + SHA_SKIPPED=0 + SHA_FAILED=0 + SHA_STATUS="unchecked" + + if [[ "${VERIFY_SHA}" == "0" ]]; then + SHA_STATUS="skipped" + echo " SHA-256 verify disabled (VERIFY_SHA=0 / --skip-sha)" + return 0 + fi + + echo " " + echo " ------ " + echo " Verifying package integrity (SHA-256 via ${label})…" + + if ! sha_has_tools; then + SHA_STATUS="no-tools" + echo " ! no local sha256 tool (sha256sum/shasum/openssl) — cannot verify" + [[ "${REQUIRE_SHA}" == "1" ]] && return 1 + return 0 + fi + + if [[ ! -s "$map" ]]; then + SHA_STATUS="no-remote" + echo " ! no digest map available — integrity not verified" + [[ "${REQUIRE_SHA}" == "1" ]] && return 1 + return 0 + fi + + for pkg in $pkgs; do + [[ -f "$pkg" ]] || { echo " ! missing package file: $pkg"; SHA_FAILED=$((SHA_FAILED+1)); continue; } + expected=$(lookupDigest "$map" "$pkg") + if [[ -z "$expected" ]]; then + SHA_SKIPPED=$((SHA_SKIPPED+1)) + echo " ~ $pkg (no digest — skip)" + continue + fi + any_expected=1 + if verifyFileSha256 "$pkg" "$expected"; then + SHA_VERIFIED=$((SHA_VERIFIED+1)) + actual=$(cat "${pkg}.sha256" 2>/dev/null || true) + echo " ✓ $pkg" + echo " sha256:${actual}" + else + SHA_FAILED=$((SHA_FAILED+1)) + actual=$(sha256_file "$pkg" 2>/dev/null || echo "?") + echo " ✗ $pkg SHA-256 MISMATCH" + echo " expected: $expected" + echo " actual: $actual" + echo " removing corrupt download" + rm -f "$pkg" "${pkg}.sha256" + fi + done + + if [[ $SHA_FAILED -gt 0 ]]; then + SHA_STATUS="failed" + echo " " + echo " ERROR: $SHA_FAILED package(s) failed SHA-256 verification." + return 1 + fi + if [[ $SHA_VERIFIED -gt 0 ]]; then + SHA_STATUS="verified" + echo " " + echo " ✓ Secure download: $SHA_VERIFIED package(s) verified SHA-256" + [[ $SHA_SKIPPED -gt 0 ]] && echo " ($SHA_SKIPPED without published digest — not checked)" + elif [[ $any_expected -eq 0 ]]; then + SHA_STATUS="no-digest" + echo " ~ no SHA digests for these packages — integrity not verified" + [[ "${REQUIRE_SHA}" == "1" ]] && return 1 + else + SHA_STATUS="skipped" + fi + return 0 +} diff --git a/scripts/emscripten/download_libs.sh b/scripts/emscripten/download_libs.sh index 611b3ac3330..5a0472bf93e 100755 --- a/scripts/emscripten/download_libs.sh +++ b/scripts/emscripten/download_libs.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash - +# Platform-clean removes only lib/emscripten; other platforms stay installed. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" if [ ! -z ${BITS+x} ]; then - ../dev/download_libs.sh -p emscripten -b -a $BITS $@ + ../dev/download_libs.sh -p emscripten -b -a $BITS "$@" else - ../dev/download_libs.sh -p emscripten -b -n $@ + ../dev/download_libs.sh -p emscripten -b "$@" fi diff --git a/scripts/ios/download_libs.sh b/scripts/ios/download_libs.sh index d2d9a8c9297..5caa8967fe2 100755 --- a/scripts/ios/download_libs.sh +++ b/scripts/ios/download_libs.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash +# Apple multi-target xcframeworks ship as the "macos" package (ios · tvos · … slices). +# Platform-clean only touches lib/macos; other platforms (android, emscripten, …) are kept. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" if [ ! -z ${BITS+x} ]; then - ../dev/download_libs.sh -p macos -b -a $BITS -n $@ + ../dev/download_libs.sh -p macos -b -a $BITS "$@" else - ../dev/download_libs.sh -p macos -b -n $@ + ../dev/download_libs.sh -p macos -b "$@" fi diff --git a/scripts/linux/download_libs.sh b/scripts/linux/download_libs.sh index 6aff8f87641..13ec2d28968 100755 --- a/scripts/linux/download_libs.sh +++ b/scripts/linux/download_libs.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Platform-clean removes only lib/linux; other platforms stay installed. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR -../dev/download_libs.sh -p linux -b $@ +cd "$SCRIPT_DIR" +../dev/download_libs.sh -p linux -b "$@" diff --git a/scripts/macos/download_libs.sh b/scripts/macos/download_libs.sh index aa7b1f6ee4e..bc772477124 100755 --- a/scripts/macos/download_libs.sh +++ b/scripts/macos/download_libs.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash - +# Downloads Apple multi-target package → lib/macos/*.xcframework +# Default clean is platform-scoped (lib/macos only); pass --full-clean for include wipe. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" if [ ! -z ${BITS+x} ]; then - ../dev/download_libs.sh -p macos -b -a $BITS $@ + ../dev/download_libs.sh -p macos -b -a $BITS "$@" else - ../dev/download_libs.sh -p macos -b -n $@ + ../dev/download_libs.sh -p macos -b "$@" fi diff --git a/scripts/msys2/download_libs.sh b/scripts/msys2/download_libs.sh index 8e9310e2201..36628ca467d 100755 --- a/scripts/msys2/download_libs.sh +++ b/scripts/msys2/download_libs.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash +# Platform-clean removes only lib/msys2; shared include/bin need --full-clean. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" -../dev/download_libs.sh -p msys2 -b $@ +../dev/download_libs.sh -p msys2 -b "$@" diff --git a/scripts/of.sh b/scripts/of.sh index 09f60624650..2bd602a9298 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # of.sh - openFrameworks CLI | Dan Rosser 2025 -OF_SCRIPT_VERSION=0.3.0 +OF_SCRIPT_VERSION=0.4.0 OF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" OF_DIR="$(realpath "$OF_DIR/../")" @@ -17,6 +17,7 @@ UI_APP_VERSION="$OF_SCRIPT_VERSION" UI_ANIM="${OF_ANIM:-1}" . "$OF_CORE_SCRIPT_DIR/ui.sh" . "$OF_CORE_SCRIPT_DIR/dev/lib_sources.sh" +. "$OF_CORE_SCRIPT_DIR/of_build.sh" OF_LIB_PLATFORMS=(osx macos ios android linux vs emscripten msys2) OF_LINUX_DISTRO="${OF_LINUX_DISTRO:-}" @@ -103,6 +104,25 @@ resolveApothecary(){ } resolveApothecary +# Host VS package arch: 64 | arm64 (for download_libs / status display) +detectHostVsArch(){ + local m pa + pa="${PROCESSOR_ARCHITECTURE:-}" + [[ -n "${PROCESSOR_ARCHITEW6432:-}" ]] && pa="${PROCESSOR_ARCHITEW6432}" + case "${MSYSTEM:-}" in + CLANGARM64|clangarm64) echo "arm64"; return 0 ;; + esac + case "$(echo "$pa" | tr '[:lower:]' '[:upper:]')" in + ARM64) echo "arm64"; return 0 ;; + AMD64|X86) echo "64"; return 0 ;; + esac + m=$(uname -m 2>/dev/null || echo "") + case "$m" in + aarch64|arm64|ARM64) echo "arm64" ;; + *) echo "64" ;; + esac +} + autoDetectOS(){ if [[ -z "$PLATFORM" ]]; then export OF_OS OF_PLATFORM OF_ARCH @@ -110,7 +130,12 @@ autoDetectOS(){ case "$OF_OS" in darwin|dawin) OF_PLATFORM="osx"; OF_ARCH=$(uname -m) ;; linux) OF_PLATFORM="linux"; OF_ARCH=$(uname -m) ;; - mingw*|cygwin*|msys*) OF_PLATFORM="vs"; OF_ARCH=${MSYSTEM,,} ;; + mingw*|cygwin*|msys*) + # Prefer VS toolchain path on Windows shells; arch is CPU not MSYSTEM name + OF_PLATFORM="vs" + OF_ARCH=$(detectHostVsArch) + export OF_MSYSTEM="${MSYSTEM:-}" + ;; *) echoError "Unsupported platform: $OF_OS"; exit 1 ;; esac else @@ -118,6 +143,9 @@ autoDetectOS(){ OF_OS=$(echo "${PLATFORM}" | tr '[:upper:]' '[:lower:]') OF_PLATFORM="$PLATFORM" OF_ARCH="" + if [[ "$OF_PLATFORM" == "vs" ]]; then + OF_ARCH=$(detectHostVsArch) + fi fi } @@ -293,14 +321,73 @@ readOfVersion(){ fi } +# True CLI projectGenerator only — never the Electron GUI host. +# Windows GUI package layout (download_pg.sh): +# projectGenerator/projectGenerator.exe → Electron GUI (opens UI; EPIPE on --version) +# projectGenerator/projectGeneratorCmd.exe → CLI copy (preferred) +# projectGenerator/resources/app/app/projectGenerator.exe → CLI +# macOS: +# projectGenerator/projectGenerator → CLI copy from app resources +# …/Contents/Resources/app/app/projectGenerator → CLI inside .app +# …/Contents/MacOS/projectGenerator → Electron host (skip) findPGBinary(){ - local p - for p in \ - "${OF_DIR}/projectGenerator/projectGenerator" \ - "${OF_PG_INSTALLED_DIR}/projectGenerator" \ - "${OF_DIR}/projectGenerator/projectGenerator.app/Contents/MacOS/projectGenerator" - do - [[ -x "$p" || -f "$p" ]] && { printf '%s' "$p"; return 0; } + local root p + local -a roots=() + [[ -n "${OF_DIR:-}" ]] && roots+=("${OF_DIR}/projectGenerator") + [[ -n "${OF_PG_INSTALLED_DIR:-}" && "${OF_PG_INSTALLED_DIR}" != "${OF_DIR}/projectGenerator" ]] && \ + roots+=("${OF_PG_INSTALLED_DIR}") + # de-dupe while preserving order + local -a seen=() uniq=() + for root in "${roots[@]}"; do + [[ -d "$root" ]] || continue + local r + r=$(cd "$root" 2>/dev/null && pwd -P) || r="$root" + local s + for s in "${seen[@]+"${seen[@]}"}"; do [[ "$s" == "$r" ]] && continue 2; done + seen+=("$r") + uniq+=("$root") + done + + for root in "${uniq[@]+"${uniq[@]}"}"; do + for p in \ + "${root}/projectGeneratorCmd.exe" \ + "${root}/projectGeneratorCmd" \ + "${root}/resources/app/app/projectGenerator.exe" \ + "${root}/resources/app/app/projectGenerator" \ + "${root}/projectGenerator.app/Contents/Resources/app/app/projectGenerator" \ + "${root}/projectGenerator" + do + # Prefer real files. On Windows, bare "projectGenerator" must NOT + # resolve via PATHEXT to projectGenerator.exe (Electron host). + if [[ -f "$p" ]]; then + # Reject Electron hosts if we accidentally hit them + case "$p" in + */Contents/MacOS/projectGenerator) continue ;; + */projectGenerator.exe) + # root-level .exe is the GUI; only accept under resources/app/app/ + [[ "$p" == */resources/app/app/projectGenerator.exe ]] || continue + ;; + esac + printf '%s' "$p" + return 0 + fi + done + done + return 1 +} + +# Optional: path to GUI (for messages only — do not exec for status/version) +findPGGui(){ + local root p + for root in "${OF_DIR}/projectGenerator" "${OF_PG_INSTALLED_DIR}"; do + [[ -d "$root" ]] || continue + for p in \ + "${root}/projectGenerator.exe" \ + "${root}/projectGenerator.app" \ + "${root}/projectGenerator.app/Contents/MacOS/projectGenerator" + do + [[ -e "$p" ]] && { printf '%s' "$p"; return 0; } + done done return 1 } @@ -308,11 +395,25 @@ findPGBinary(){ readPGVersion(){ local bin ver bin=$(findPGBinary) || return 1 - ver=$("$bin" --version 2>/dev/null | head -1 | tr -d '\r') - [[ -z "$ver" ]] && ver=$("$bin" -v 2>/dev/null | head -1 | tr -d '\r') + # Bound runtime so a mis-resolved GUI cannot hang status (EPIPE / Electron) + if command -v timeout >/dev/null 2>&1; then + ver=$(timeout 5s "$bin" --version 2>/dev/null | head -1 | tr -d '\r') + [[ -z "$ver" ]] && ver=$(timeout 5s "$bin" -v 2>/dev/null | head -1 | tr -d '\r') + elif command -v gtimeout >/dev/null 2>&1; then + ver=$(gtimeout 5s "$bin" --version 2>/dev/null | head -1 | tr -d '\r') + [[ -z "$ver" ]] && ver=$(gtimeout 5s "$bin" -v 2>/dev/null | head -1 | tr -d '\r') + else + ver=$("$bin" --version 2>/dev/null | head -1 | tr -d '\r') + [[ -z "$ver" ]] && ver=$("$bin" -v 2>/dev/null | head -1 | tr -d '\r') + fi if [[ "$ver" == *projectGenerator* ]]; then ver=$(printf '%s' "$ver" | sed -E 's/.*"openFrameworks projectGenerator": *"([^"]+)".*/\1/') fi + # Electron sometimes prints garbage / empty when invoked as CLI + if [[ -z "$ver" || "$ver" == *"Electron"* || "$ver" == *"chrome-error"* ]]; then + printf '%s' "installed (CLI: $(basename "$bin"))" + return 0 + fi printf '%s' "${ver:-installed}" } @@ -655,6 +756,22 @@ cmdStatus(){ fi echoKV "last update" "$lastUp" echoKV "cli" "$OF_SCRIPT_VERSION" + local stSha stShaN + stSha=$(readLibStateField sha_status) || stSha=$(readVerifyField status) || stSha="" + stShaN=$(readLibStateField sha_verified) || stShaN=$(readVerifyField verified) || stShaN="" + case "$stSha" in + verified) + if [[ -n "$stShaN" && "$stShaN" != "0" ]]; then + echoKV "integrity" "SHA-256 verified (${stShaN} package(s)) · secure" + else + echoKV "integrity" "SHA-256 verified · secure" + fi + ;; + failed) echoKV "integrity" "SHA-256 failed" ;; + no-digest|no-remote) echoKV "integrity" "SHA not available for last download" ;; + no-tools) echoKV "integrity" "no local sha256 tool" ;; + skipped) echoKV "integrity" "SHA check skipped" ;; + esac printLibInlineSection "Core" "${SEC_CORE[@]}" printLibInlineSection "Addons" "${SEC_ADDONS[@]}" @@ -708,16 +825,20 @@ printHelp(){ status System checker + full library list setup Install deps/libs/PG if missing or outdated update libs | pg | all Prompt source; refresh libs/PG as needed + build … Build core / projects / emscripten / cmake (see of build help) + cleanup projects|caches|libs Free disk (artifacts / downloads / prebuilts) version of | pg Version info upgrade addons | apps Upgrade tree installed Alias for status - apothecary Build via apothecary submodule + apothecary Build libraries via apothecary submodule Env LIB_SOURCE=apothecary|oflibs|archive LIB_TAG=latest|v12.1.0|v0.11.2 LIB_LIBS=core|all|"glfw glm" + LIB_CLEAN_MODE=platform|merge|full # default platform: only lib/ OF_LINUX_DISTRO=ubuntu Force linux distro scripts + OF_JOBS=8 Parallel build jobs VERBOSE=1 NO_COLOR=1 OF_ANIM=0 Examples @@ -725,11 +846,18 @@ printHelp(){ ${prog} status ${prog} update libs LIB_SOURCE=oflibs LIB_LIBS=core ${prog} update libs + ${prog} build core Debug + ${prog} build project apps/myApps/mySketch + ${prog} build emscripten examples/graphics/graphicsExample + ${prog} cleanup projects + ${prog} cleanup caches + ${prog} cleanup libs other ${prog} apothecary build EOF echoKV "platform" "${OF_PLATFORM}${OF_ARCH:+ / ${OF_ARCH}}" echoKV "libs source" "${LIB_SOURCE} / ${LIB_TAG}" + echoNote "build details: ${prog} build help" printf '\n' } @@ -765,10 +893,22 @@ OF_LIB_STATE_FILE="${OF_DIR}/libs/.of-cli-state" # Essentials used to decide "installed" OF_LIBS_ESSENTIAL=(freetype glew glfw zlib tess2 uriparser utf8 json) +# Merge last SHA verify result from download_libs (libs/download/.last-verify) +readVerifyField(){ + local key="$1" + local f="${OF_DIR}/libs/download/.last-verify" + [[ -f "$f" ]] || return 1 + grep -E "^${key}=" "$f" 2>/dev/null | head -1 | cut -d= -f2- +} + writeLibState(){ local source="${1:-$LIB_SOURCE}" local tag="${2:-$LIB_TAG}" local platformDir="${3:-$OF_PLATFORM}" + local shaStatus shaVerified shaUpdated + shaStatus=$(readVerifyField status) || shaStatus="" + shaVerified=$(readVerifyField verified) || shaVerified="" + shaUpdated=$(readVerifyField updated) || shaUpdated="" mkdir -p "${OF_DIR}/libs" cat > "$OF_LIB_STATE_FILE" << EOF source=${source} @@ -777,6 +917,9 @@ platform=${platformDir} arch=${OF_ARCH:-} updated=$(date -u +%Y-%m-%dT%H:%M:%SZ) of_cli=${OF_SCRIPT_VERSION} +sha_status=${shaStatus} +sha_verified=${shaVerified} +sha_updated=${shaUpdated} EOF } @@ -825,23 +968,44 @@ assessLibsState(){ stSource=$(readLibStateField source) || stSource="apothecary" stTag=$(readLibStateField tag) || stTag="latest" stUpdated=$(readLibStateField updated) || stUpdated="" + local stSha stShaN + stSha=$(readLibStateField sha_status) || stSha=$(readVerifyField status) || stSha="" + stShaN=$(readLibStateField sha_verified) || stShaN=$(readVerifyField verified) || stShaN="" LIBS_STATE="ok" if [[ -n "$stUpdated" ]]; then LIBS_STATE_DETAIL="${stSource}@${stTag} · ${stUpdated}" else LIBS_STATE_DETAIL="detected · assume latest" fi + case "$stSha" in + verified) + if [[ -n "$stShaN" && "$stShaN" != "0" ]]; then + LIBS_STATE_DETAIL+=" · SHA-256 secure (${stShaN} pkg)" + else + LIBS_STATE_DETAIL+=" · SHA-256 secure" + fi + ;; + failed) LIBS_STATE_DETAIL+=" · SHA failed" ;; + no-digest|no-remote|no-tools|skipped|unchecked|"") ;; + *) LIBS_STATE_DETAIL+=" · SHA ${stSha}" ;; + esac } # Sets PG_STATE=ok|missing PG_STATE_DETAIL= assessPGState(){ - local bin ver + local bin ver gui PG_STATE="ok" PG_STATE_DETAIL="" bin=$(findPGBinary) || bin="" if [[ -z "$bin" ]]; then PG_STATE="missing" - PG_STATE_DETAIL="Project Generator not installed" + gui=$(findPGGui) || gui="" + if [[ -n "$gui" ]]; then + # Windows/mac GUI package present but CLI path missing (old layout / incomplete extract) + PG_STATE_DETAIL="GUI found, CLI missing — re-run: of update pg (need projectGeneratorCmd.exe)" + else + PG_STATE_DETAIL="Project Generator not installed" + fi return 0 fi ver=$(readPGVersion) || ver="installed" @@ -1145,6 +1309,20 @@ menuDownloadLibs(){ echoNote "macos packages → lib/macos/*.xcframework (osx · ios · tvos · …)" ;; esac + # Default is platform-scoped clean (safe multi-platform). Optional full wipe of shared include/. + if menuCanRun; then + menuPick "Clean mode before install · ${platformDir}" \ + "Platform only — keep other platforms (Recommended)|platform" \ + "Merge only — never delete before extract|merge" \ + "Full clean — also wipe shared include/ (+ bin on vs/msys2)|full" \ + || return 1 + case "$UI_MENU_RESULT" in + merge) export LIB_CLEAN_MODE=merge ;; + full) export LIB_CLEAN_MODE=full ;; + *) export LIB_CLEAN_MODE=platform ;; + esac + echoSuccess "clean → $LIB_CLEAN_MODE" + fi fi [[ "$LIB_SOURCE" == "oflibs" ]] && { menuPickOfLibs "$LIB_TAG" "$platformDir" || return 1; } @@ -1160,7 +1338,7 @@ archesForApoType(){ android) echo "arm64 armv7 x86 x86_64" ;; emscripten) echo "64 32" ;; linux) echo "64 aarch64 armv7l armv6l" ;; - vs) echo "64 arm64" ;; + vs) echo "64 arm64 arm64ec all" ;; msys2) echo "64" ;; *) echo "${OF_ARCH:-64}" ;; esac @@ -1441,6 +1619,315 @@ cmdSetup(){ printf '\n' } +# --------------------------------------------------------------------------- +# Cleanup — projects / caches / libraries +# --------------------------------------------------------------------------- + +# Clean build products under a project tree (obj, bin artifacts, Xcode/VS/cmake) +cleanProjectTree(){ + local project="$1" + local deep="${2:-0}" + [[ -d "$project" ]] || return 1 + # Prefer makefile clean when available + if [[ -f "${project}/Makefile" ]] || [[ -f "${project}/makefile" ]]; then + ( cd "$project" && make clean >/dev/null 2>&1 ) || true + fi + # Common OF build leftovers + rm -rf "${project}/obj" \ + "${project}/bin/"*.app \ + "${project}/bin/"*_debug \ + "${project}/bin/"*_debug.exe \ + "${project}/build" \ + "${project}/cmake-build-debug" \ + "${project}/cmake-build-release" \ + "${project}/.vs" \ + "${project}/DerivedData" 2>/dev/null || true + # Xcode user data / indexes (not the .xcodeproj itself) + find "$project" -maxdepth 3 \( \ + -name 'xcuserdata' -o -name 'project.xcworkspace' -o \ + -name '*.o' -o -name '*.d' -o -name '*.depend' -o -name '*.layout' \ + \) -exec rm -rf {} + 2>/dev/null || true + # VS intermediate + find "$project" -maxdepth 3 \( \ + -name 'x64' -o -name 'ARM64' -o -name 'Win32' -o \ + -name 'Debug' -o -name 'Release' \ + \) -type d \( -path '*/obj/*' -o -path '*/bin/*' -o -name 'obj' \) \ + -exec rm -rf {} + 2>/dev/null || true + find "$project" -maxdepth 2 \( -name '*.exe' -o -name '*.pdb' -o -name '*.ilk' -o -name '*.obj' \) \ + -path '*/bin/*' -delete 2>/dev/null || true + if [[ "$deep" == "1" ]]; then + # also strip empty bin/ data keepers stay if only data/ + find "${project}/bin" -mindepth 1 -maxdepth 1 ! -name 'data' -exec rm -rf {} + 2>/dev/null || true + fi + return 0 +} + +# Walk apps/ or examples/ (or a custom root) and clean each project-like folder +cleanProjectsScope(){ + local scope="$1" # apps|examples|all|path + local path="${2:-}" + local deep="${3:-0}" + local root count=0 + local -a roots=() + + case "$scope" in + apps) roots+=("${OF_DIR}/apps") ;; + examples) roots+=("${OF_DIR}/examples") ;; + all) roots+=("${OF_DIR}/apps" "${OF_DIR}/examples") ;; + path) + [[ -n "$path" && -d "$path" ]] || { echoError "path required"; return 1; } + roots+=("$path") + ;; + *) echoError "scope: apps|examples|all|path"; return 1 ;; + esac + + printBanner "clean" + echoInfo "cleanup projects · ${scope}${path:+ · $path}" + echoKV "deep" "$([[ "$deep" == "1" ]] && echo yes || echo no)" + printf '\n' + + local cat ex proj rel + for root in "${roots[@]}"; do + [[ -d "$root" ]] || continue + # category / project (apps/myApps/foo, examples/graphics/bar) + for cat in "$root"/*/; do + [[ -d "$cat" ]] || continue + # direct project under category + if [[ -d "${cat}src" || -f "${cat}Makefile" ]]; then + proj="${cat%/}" + rel="${proj#"$OF_DIR"/}" + echoNote "clean ${rel}" + cleanProjectTree "$proj" "$deep" + count=$((count + 1)) + continue + fi + for ex in "$cat"*/; do + [[ -d "$ex" ]] || continue + if [[ -d "${ex}src" || -f "${ex}Makefile" || -f "${ex}makefile" ]] \ + || compgen -G "${ex}*.xcodeproj" >/dev/null 2>&1 \ + || compgen -G "${ex}*.vcxproj" >/dev/null 2>&1; then + proj="${ex%/}" + rel="${proj#"$OF_DIR"/}" + echoNote "clean ${rel}" + cleanProjectTree "$proj" "$deep" + count=$((count + 1)) + fi + done + done + done + echoSuccess "cleaned ${count} project folder(s)" +} + +cleanDownloadCaches(){ + local mode="${1:-packages}" # packages|all + local dl="${OF_DIR}/libs/download" + local freed=0 + printBanner "clean" + echoInfo "clean caches · ${mode}" + printf '\n' + if [[ ! -d "$dl" ]]; then + echoNote "no libs/download cache" + return 0 + fi + case "$mode" in + packages) + # package archives + sidecars; keep folder + local f n=0 + while IFS= read -r f; do + [[ -f "$f" ]] || continue + rm -f "$f" + n=$((n + 1)) + done < <(find "$dl" -type f \( \ + -name '*.tar.bz2' -o -name '*.tar.gz' -o -name '*.zip' -o \ + -name '*.sha256' -o -name 'SHA256SUMS' -o -name '.last-verify' \ + \) 2>/dev/null) + echoSuccess "removed ${n} cached package file(s) under libs/download" + ;; + all) + echoWarning "removing entire libs/download/" + rm -rf "$dl" + mkdir -p "$dl" + echoSuccess "libs/download cleared" + ;; + *) echoError "mode: packages|all"; return 1 ;; + esac + # optional compile junk under openFrameworksCompiled + if [[ "$mode" == "all" ]]; then + find "${OF_DIR}/libs/openFrameworksCompiled" -type d \( -name 'obj' -o -name 'intermediates' \) \ + -exec rm -rf {} + 2>/dev/null || true + echoNote "also cleared openFrameworksCompiled obj/intermediates if present" + fi +} + +# Remove prebuilt library binaries (minimise disk) — never deletes openFrameworks source +cleanLibraries(){ + local mode="$1" # other-platforms|platform|all-prebuilt|list + local plat="${2:-}" + local libDir d name p count=0 + + printBanner "clean" + echoInfo "libraries · ${mode}${plat:+ · $plat}" + printf '\n' + + case "$mode" in + list) + echoNote "installed lib/ folders:" + find "${OF_DIR}/libs" "${OF_DIR}/addons" -type d -path '*/lib/*' -mindepth 3 -maxdepth 4 2>/dev/null \ + | sed "s|^${OF_DIR}/||" | sort -u | head -80 + return 0 + ;; + other-platforms) + # keep host-related platform dirs; remove the rest + local keep="" + case "$OF_PLATFORM" in + vs) keep="vs" ;; + osx|macos) keep="macos|osx" ;; + ios) keep="macos|ios" ;; + android) keep="android" ;; + emscripten) keep="emscripten" ;; + msys2) keep="msys2|vs" ;; + linux) keep="linux|linux64|linuxaarch64" ;; + *) keep="$OF_PLATFORM" ;; + esac + echoKV "keep matching" "$keep" + confirmYes "Remove lib/* folders that are NOT for this host (${OF_PLATFORM})?" || { + echoInfo "cancelled"; return 0 + } + while IFS= read -r d; do + name=$(basename "$d") + if echo "$name" | grep -Eq "^(${keep})$"; then + continue + fi + # skip non-platform utility dirs + case "$name" in + pkgconfig|cmake|cmake-build*|include) continue ;; + esac + echoNote "rm $d" + rm -rf "$d" + count=$((count + 1)) + done < <(find "${OF_DIR}/libs" "${OF_DIR}/addons" -type d -path '*/lib/*' -mindepth 3 -maxdepth 4 2>/dev/null) + echoSuccess "removed ${count} non-host platform lib folder(s)" + ;; + platform) + [[ -n "$plat" ]] || { echoError "platform name required"; return 1; } + confirmYes "Remove all libs/*/lib/${plat} and addons …/lib/${plat}?" || { + echoInfo "cancelled"; return 0 + } + while IFS= read -r d; do + echoNote "rm $d" + rm -rf "$d" + count=$((count + 1)) + done < <(find "${OF_DIR}/libs" "${OF_DIR}/addons" -type d -path "*/lib/${plat}" 2>/dev/null) + echoSuccess "removed ${count} path(s) for platform ${plat}" + ;; + all-prebuilt) + echoWarning "This deletes downloaded prebuilts under libs/* and addon libs binaries." + echoWarning "Keeps: libs/openFrameworks, libs/openFrameworksCompiled (source/project)." + confirmYes "Really remove all other libs/* trees?" || { echoInfo "cancelled"; return 0; } + for d in "${OF_DIR}/libs"/*; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + case "$name" in + openFrameworks|openFrameworksCompiled|download|scripts) continue ;; + esac + echoNote "rm libs/${name}" + rm -rf "$d" + count=$((count + 1)) + done + # addon binary folders only + for d in "${OF_DIR}/addons"/*/libs; do + [[ -d "$d" ]] || continue + echoNote "rm ${d#"$OF_DIR"/}" + rm -rf "$d" + count=$((count + 1)) + done + rm -f "${OF_DIR}/libs/.of-cli-state" 2>/dev/null || true + echoSuccess "removed ${count} prebuilt tree(s) — run Update libs to restore" + ;; + *) echoError "mode: list|other-platforms|platform|all-prebuilt"; return 1 ;; + esac +} + +menuCleanup(){ + local choice scope deep=0 plat + printBanner "clean" + echoInfo "cleanup projects · caches · libraries" + printf '\n' + + while true; do + menuPick "Cleanup" \ + "Projects — obj/bin/build artifacts|projects" \ + "Caches — libs/download packages|caches" \ + "Libraries — remove / minimise prebuilts|libs" \ + "Back|back" \ + || return 0 + choice="$UI_MENU_RESULT" + case "$choice" in + projects) + menuPick "Project cleanup scope" \ + "All apps + examples|all" \ + "apps/ only|apps" \ + "examples/ only|examples" \ + "Single path…|path" \ + "Back|back" || continue + scope="$UI_MENU_RESULT" + [[ "$scope" == "back" ]] && continue + if menuCanRun && confirmNo "Also clear bin/ executables (keep bin/data)?"; then + deep=1 + else + deep=0 + fi + if [[ "$scope" == "path" ]]; then + menuInput "Project or folder path" || continue + confirmYes "Clean projects under ${UI_INPUT_RESULT}?" || continue + cleanProjectsScope path "$UI_INPUT_RESULT" "$deep" + else + confirmYes "Clean ${scope} project build artifacts?" || continue + cleanProjectsScope "$scope" "" "$deep" + fi + menuPause + ;; + caches) + menuPick "Cache cleanup" \ + "Package downloads only (Recommended)|packages" \ + "Wipe entire libs/download + compiled intermediates|all" \ + "Back|back" || continue + [[ "$UI_MENU_RESULT" == "back" ]] && continue + confirmYes "Delete download cache (${UI_MENU_RESULT})?" || continue + cleanDownloadCaches "$UI_MENU_RESULT" + menuPause + ;; + libs) + menuPick "Library cleanup" \ + "List installed lib/ folders|list" \ + "Remove other platforms (keep this host) — Recommended|other" \ + "Remove one platform…|one" \ + "Remove ALL prebuilt libs (nuclear)|all" \ + "Back|back" || continue + case "$UI_MENU_RESULT" in + list) cleanLibraries list; menuPause ;; + other) cleanLibraries other-platforms; menuPause ;; + one) + menuPick "Platform folder to remove" \ + "vs|vs" "macos|macos" "osx|osx" "android|android" \ + "emscripten|emscripten" "msys2|msys2" "linux|linux" \ + "ios|ios" "Custom…|custom" || continue + plat="$UI_MENU_RESULT" + if [[ "$plat" == "custom" ]]; then + menuInput "lib/ folder" || continue + plat="$UI_INPUT_RESULT" + fi + cleanLibraries platform "$plat" + menuPause + ;; + all) cleanLibraries all-prebuilt; menuPause ;; + esac + ;; + back) return 0 ;; + esac + done +} + cmdMenu(){ local choice setupLabel="Setup" if ! menuCanRun; then @@ -1464,7 +1951,9 @@ cmdMenu(){ "Status — system checker|status" \ "${setupLabel}|setup" \ "Update — libs / PG (choose source)|update" \ + "Build… — core / projects / examples / emscripten / cmake|build" \ "Build libraries (Apothecary)…|apothecary" \ + "Cleanup — projects / caches / libraries|cleanup" \ "Show openFrameworks version|version" \ "Show Project Generator version|version-pg" \ "Upgrade addons|upgrade-addons" \ @@ -1481,7 +1970,9 @@ cmdMenu(){ status) cmdStatus; menuPause ;; setup) cmdSetup "$OF_PLATFORM"; menuPause ;; update) menuUpdate; menuPause ;; + build) menuBuild ;; apothecary) menuApothecary; menuPause ;; + cleanup|clean) menuCleanup ;; version) cmdVersion; menuPause ;; version-pg) cmdVersionPG; menuPause ;; upgrade-addons) cmdUpgrade addons; menuPause ;; @@ -1524,6 +2015,88 @@ menuUpdate(){ esac } +# of build … +cmdBuild(){ + local sub="${1:-}" + shift || true + case "$sub" in + ""|menu) menuBuild ;; + help|-h|--help) printHelpBuild ;; + core|lib|library) + cmdBuildCore "${1:-Release}" + ;; + project|app|proj) + local path="${1:-}" system="make" config="Release" + [[ -n "$path" ]] || { echoError "usage: of build project [system] [Debug|Release]"; return 1; } + shift || true + # optional system then config, or config alone + if [[ "${1:-}" =~ ^(make|xcode|xcodebuild|emscripten|em|wasm|cmake|generate|pg|host)$ ]]; then + system="$1"; shift || true + fi + [[ -n "${1:-}" ]] && config="$1" + cmdBuildProject "$path" "$system" "$config" 0 + ;; + example|examples) + local path="${1:-}" system="make" config="Release" + [[ -n "$path" ]] || { echoError "usage: of build example [system] [cfg]"; return 1; } + shift || true + if [[ "${1:-}" =~ ^(make|xcode|xcodebuild|emscripten|em|wasm|cmake|generate|pg|host)$ ]]; then + system="$1"; shift || true + fi + [[ -n "${1:-}" ]] && config="$1" + # allow short example paths + if [[ ! -d "$path" && -d "${OF_DIR}/examples/$path" ]]; then + path="${OF_DIR}/examples/$path" + fi + cmdBuildProject "$path" "$system" "$config" 0 + ;; + emscripten|em|wasm) + local path="${1:-}" config="${2:-Debug}" + [[ -n "$path" ]] || { echoError "usage: of build emscripten [Debug|Release]"; return 1; } + cmdBuildProject "$path" emscripten "$config" 0 + ;; + open-em|emrun|open-emscripten) + local path="${1:-}" + [[ -n "$path" ]] || { echoError "usage: of build open-em "; return 1; } + path=$(resolveProjectPath "$path") || { echoError "not found: $1"; return 1; } + ensureEmscriptenEnv || true + runEmscriptenOpen "$path" + ;; + generate|pg) + local path="${1:-}" platforms="${2:-}" + [[ -n "$path" ]] || { echoError "usage: of build generate [platforms]"; return 1; } + cmdBuildGenerate "$path" "$platforms" + ;; + cmake) + case "${1:-}" in + ""|status|info) runCmakeStatus ;; + android) runCmakeAndroidCore "${2:-Debug}" ;; + *) + local path="$1" config="${2:-Release}" + path=$(resolveProjectPath "$path") || { echoError "not found: $1"; return 1; } + runCmakeProject "$path" "$config" + ;; + esac + ;; + clean) + local path="${1:-}" + [[ -n "$path" ]] || { echoError "usage: of build clean "; return 1; } + path=$(resolveProjectPath "$path") || { echoError "not found: $1"; return 1; } + runCleanProject "$path" + ;; + *) + # bare path → treat as project with make + if [[ -n "$sub" ]] && { [[ -d "$sub" ]] || [[ -d "${OF_DIR}/$sub" ]] || [[ -d "${OF_DIR}/examples/$sub" ]] || [[ -d "${OF_DIR}/apps/$sub" ]]; }; then + cmdBuildProject "$sub" "${1:-make}" "${2:-Release}" 0 + return $? + fi + echoError "unknown build subcommand: ${sub:-}" + printHelpBuild + return 1 + ;; + esac +} + runCommand(){ local cmd=$1 subcmd=$2 subcmd2=$3 subcmd3=$4 case "$cmd" in @@ -1532,6 +2105,10 @@ runCommand(){ status|check|doctor|installed|libs-status) cmdStatus ;; setup|install) cmdSetup "${subcmd:-$OF_PLATFORM}" ;; update) cmdUpdate "$subcmd" "$subcmd2" ;; + build) + shift + cmdBuild "$@" + ;; version) case "$subcmd" in ""|of) cmdVersion ;; @@ -1540,6 +2117,35 @@ runCommand(){ esac ;; upgrade) cmdUpgrade "$subcmd" ;; + cleanup|clean) + case "${subcmd:-}" in + ""|menu) menuCleanup ;; + projects|project) + case "${subcmd2:-all}" in + apps|examples|all) cleanProjectsScope "${subcmd2:-all}" "" 0 ;; + *) cleanProjectsScope path "${subcmd2}" 0 ;; + esac + ;; + caches|cache) + cleanDownloadCaches "${subcmd2:-packages}" + ;; + libs|libraries) + case "${subcmd2:-other}" in + list) cleanLibraries list ;; + other|other-platforms|host) cleanLibraries other-platforms ;; + all|nuclear) cleanLibraries all-prebuilt ;; + *) cleanLibraries platform "${subcmd2}" ;; + esac + ;; + *) + echoError "usage: of cleanup [projects|caches|libs] …" + echoNote " of cleanup projects [all|apps|examples|]" + echoNote " of cleanup caches [packages|all]" + echoNote " of cleanup libs [other|list|all|vs|macos|…]" + return 1 + ;; + esac + ;; apothecary|apo) case "$subcmd" in ""|menu) menuApothecary ;; @@ -1550,7 +2156,7 @@ runCommand(){ ;; *) echoError "Unknown command: $cmd" - echoNote "valid: menu status setup update version upgrade apothecary help" + echoNote "valid: menu status setup update build cleanup version upgrade apothecary help" printHelp return 1 ;; diff --git a/scripts/of_build.sh b/scripts/of_build.sh new file mode 100644 index 00000000000..837a231694c --- /dev/null +++ b/scripts/of_build.sh @@ -0,0 +1,1030 @@ +#!/usr/bin/env bash +# of_build.sh — interactive + CLI project build helpers for of.sh +# Sourced by scripts/of.sh (expects OF_DIR, UI helpers, findPGBinary, etc.) + +# Parallel jobs for make / cmake / xcodebuild +OF_JOBS="${OF_JOBS:-}" +ofBuildJobs(){ + if [[ -n "$OF_JOBS" ]]; then + printf '%s' "$OF_JOBS" + return + fi + local n + if command -v nproc >/dev/null 2>&1; then + n=$(nproc 2>/dev/null) + elif [[ "$(uname -s)" == "Darwin" ]]; then + n=$(sysctl -n hw.ncpu 2>/dev/null) + fi + [[ -z "$n" || "$n" -lt 1 ]] && n=4 + printf '%s' "$n" +} + +# Host PG platform token (osx on Darwin, linux64/linux, msys2/vs) +ofHostPgPlatform(){ + case "$OF_PLATFORM" in + osx|macos) printf 'osx' ;; + linux) + case "$OF_ARCH" in + x86_64|amd64) printf 'linux64' ;; + aarch64|arm64) printf 'linuxaarch64' ;; + armv7l) printf 'linuxarmv7l' ;; + armv6l) printf 'linuxarmv6l' ;; + *) printf 'linux64' ;; + esac + ;; + msys2) printf 'msys2' ;; + vs) printf 'vs' ;; + *) printf '%s' "$OF_PLATFORM" ;; + esac +} + +# --------------------------------------------------------------------------- +# Tool discovery +# --------------------------------------------------------------------------- +ensureEmscriptenEnv(){ + if command -v emmake >/dev/null 2>&1 && command -v emcc >/dev/null 2>&1; then + return 0 + fi + local f + for f in \ + "${EMSDK}/emsdk_env.sh" \ + "${HOME}/emsdk/emsdk_env.sh" \ + "${OF_DIR}/emsdk/emsdk_env.sh" \ + "/opt/homebrew/share/emscripten/emsdk_env.sh" \ + "/usr/local/share/emscripten/emsdk_env.sh" + do + [[ -n "${EMSDK:-}" && "$f" == "${EMSDK}/emsdk_env.sh" && ! -f "$f" ]] && continue + if [[ -f "$f" ]]; then + # shellcheck disable=SC1090 + . "$f" >/dev/null 2>&1 || true + break + fi + done + command -v emmake >/dev/null 2>&1 && command -v emcc >/dev/null 2>&1 +} + +# Prints browser id for emrun, or empty +findChromeForEmrun(){ + if command -v google-chrome >/dev/null 2>&1 || command -v google-chrome-stable >/dev/null 2>&1; then + printf 'chrome' + return 0 + fi + if command -v chromium >/dev/null 2>&1 || command -v chromium-browser >/dev/null 2>&1; then + printf 'chromium' + return 0 + fi + if [[ -d "/Applications/Google Chrome.app" ]]; then + printf 'chrome' + return 0 + fi + if [[ -d "/Applications/Chromium.app" ]]; then + printf 'chromium' + return 0 + fi + return 1 +} + +findSafariForEmrun(){ + [[ -d "/Applications/Safari.app" ]] && { printf 'safari'; return 0; } + return 1 +} + +# --------------------------------------------------------------------------- +# Project discovery +# --------------------------------------------------------------------------- +isOfProjectDir(){ + local d="$1" + [[ -d "$d" ]] || return 1 + [[ -d "$d/src" ]] && return 0 + [[ -f "$d/Makefile" || -f "$d/addons.make" || -f "$d/config.make" ]] && return 0 + [[ -f "$d/CMakeLists.txt" ]] && return 0 + # shellcheck disable=SC2086 + compgen -G "$d/*.xcodeproj" >/dev/null 2>&1 && return 0 + compgen -G "$d/*.sln" >/dev/null 2>&1 && return 0 + return 1 +} + +# List immediate child dirs of $1 that look like OF projects +listOfProjectChildren(){ + local root="$1" d + [[ -d "$root" ]] || return 0 + for d in "$root"/*/; do + [[ -d "$d" ]] || continue + d="${d%/}" + isOfProjectDir "$d" && printf '%s\n' "$d" + done | sort +} + +# Categories under examples/ (skip platform-only folders when listing generic) +listExampleCategories(){ + local d name + for d in "${OF_DIR}/examples"/*/; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + [[ "$name" == "README.md" ]] && continue + printf '%s\n' "$name" + done | sort +} + +listAppGroups(){ + local d name + for d in "${OF_DIR}/apps"/*/; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + printf '%s\n' "$name" + done | sort +} + +listAddons(){ + local d name + for d in "${OF_DIR}/addons"/ofx*/; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + printf '%s\n' "$name" + done | sort +} + +# example* / Example* dirs under an addon (depth 1–2) +listAddonExamples(){ + local addonRoot="$1" d + [[ -d "$addonRoot" ]] || return 0 + find "$addonRoot" -maxdepth 2 -type d \( \ + -name 'example*' -o -name 'Example*' -o -name 'example-*' \ + \) 2>/dev/null | sort +} + +# Resolve relative or absolute project path → absolute under OF or absolute +resolveProjectPath(){ + local p="$1" + [[ -n "$p" ]] || return 1 + if [[ -d "$p" ]]; then + realpath "$p" + return 0 + fi + if [[ -d "${OF_DIR}/$p" ]]; then + realpath "${OF_DIR}/$p" + return 0 + fi + if [[ -d "${OF_DIR}/examples/$p" ]]; then + realpath "${OF_DIR}/examples/$p" + return 0 + fi + if [[ -d "${OF_DIR}/apps/$p" ]]; then + realpath "${OF_DIR}/apps/$p" + return 0 + fi + return 1 +} + +# --------------------------------------------------------------------------- +# Interactive pickers +# --------------------------------------------------------------------------- +# Sets UI_MENU_RESULT to absolute project path +menuPickExample(){ + local cat name path + local -a opts=() + local c + while IFS= read -r c; do + [[ -n "$c" ]] && opts+=("${c}|${c}") + done < <(listExampleCategories) + [[ ${#opts[@]} -eq 0 ]] && { echoError "no examples/ categories"; return 1; } + opts+=("Back|__back__") + menuPick "Example category" "${opts[@]}" || return 1 + cat="$UI_MENU_RESULT" + [[ "$cat" == "__back__" ]] && return 1 + + opts=() + while IFS= read -r path; do + name=$(basename "$path") + opts+=("${name}|${path}") + done < <(listOfProjectChildren "${OF_DIR}/examples/${cat}") + [[ ${#opts[@]} -eq 0 ]] && { echoError "no projects in examples/${cat}"; return 1; } + opts+=("Back|__back__") + menuPick "Example · ${cat}" "${opts[@]}" || return 1 + [[ "$UI_MENU_RESULT" == "__back__" ]] && return 1 + return 0 +} + +menuPickAppProject(){ + local group name path + local -a opts=() + local g + while IFS= read -r g; do + [[ -n "$g" ]] && opts+=("${g}|${g}") + done < <(listAppGroups) + [[ ${#opts[@]} -eq 0 ]] && { echoError "no apps/ groups"; return 1; } + opts+=("Custom path…|__path__" "Back|__back__") + menuPick "App group (apps/…)" "${opts[@]}" || return 1 + group="$UI_MENU_RESULT" + [[ "$group" == "__back__" ]] && return 1 + if [[ "$group" == "__path__" ]]; then + menuInput "Project path (relative to OF root or absolute)" "apps/myApps/" || return 1 + path=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not a directory: $UI_INPUT_RESULT" + return 1 + } + UI_MENU_RESULT="$path" + return 0 + fi + + opts=() + # prefer projects directly under group; also one level deeper (myApps/foo) + while IFS= read -r path; do + name=$(basename "$path") + opts+=("${name}|${path}") + done < <(listOfProjectChildren "${OF_DIR}/apps/${group}") + + # nested: group/sub/project + local sub + for sub in "${OF_DIR}/apps/${group}"/*/; do + [[ -d "$sub" ]] || continue + isOfProjectDir "${sub%/}" && continue + while IFS= read -r path; do + name="$(basename "$(dirname "$path")")/$(basename "$path")" + opts+=("${name}|${path}") + done < <(listOfProjectChildren "${sub%/}") + done + + [[ ${#opts[@]} -eq 0 ]] && { echoError "no projects under apps/${group}"; return 1; } + opts+=("Back|__back__") + menuPick "Project · apps/${group}" "${opts[@]}" || return 1 + [[ "$UI_MENU_RESULT" == "__back__" ]] && return 1 + return 0 +} + +menuPickAddonExample(){ + local addon path name + local -a opts=() + local a + while IFS= read -r a; do + [[ -n "$a" ]] && opts+=("${a}|${a}") + done < <(listAddons) + [[ ${#opts[@]} -eq 0 ]] && { echoError "no addons/ofx*"; return 1; } + opts+=("Back|__back__") + menuPick "Addon" "${opts[@]}" || return 1 + addon="$UI_MENU_RESULT" + [[ "$addon" == "__back__" ]] && return 1 + + opts=() + while IFS= read -r path; do + [[ -d "$path" ]] || continue + # skip if not project-like when empty + if isOfProjectDir "$path" || [[ -d "$path/src" ]]; then + name="${path#${OF_DIR}/addons/${addon}/}" + opts+=("${name}|${path}") + fi + done < <(listAddonExamples "${OF_DIR}/addons/${addon}") + + # also any ofx* folder that has example-style children already covered; + # if none, list any child with src/ + if [[ ${#opts[@]} -eq 0 ]]; then + while IFS= read -r path; do + name=$(basename "$path") + opts+=("${name}|${path}") + done < <(listOfProjectChildren "${OF_DIR}/addons/${addon}") + fi + + [[ ${#opts[@]} -eq 0 ]] && { + echoError "no examples under addons/${addon}" + echoNote "core addons often ship without examples — try examples/ or apps/" + return 1 + } + opts+=("Back|__back__") + menuPick "Addon example · ${addon}" "${opts[@]}" || return 1 + [[ "$UI_MENU_RESULT" == "__back__" ]] && return 1 + return 0 +} + +menuPickConfig(){ + local -a opts=( + "Debug|Debug" + "Release|Release" + ) + menuPick "Configuration" "${opts[@]}" || return 1 +} + +menuPickBuildSystem(){ + local project="$1" + local -a opts=() + local hostPg + hostPg=$(ofHostPgPlatform) + + opts+=("make (${hostPg} / host)|make") + if [[ "$(uname -s)" == "Darwin" ]] && compgen -G "${project}/*.xcodeproj" >/dev/null 2>&1; then + opts+=("xcodebuild|xcode") + fi + opts+=("emscripten (emmake + optional browser)|emscripten") + if [[ -f "${project}/CMakeLists.txt" ]] || command -v cmake >/dev/null 2>&1; then + opts+=("cmake|cmake") + fi + opts+=("Generate project files only (PG)|generate") + menuPick "Build system · $(basename "$project")" "${opts[@]}" || return 1 +} + +# --------------------------------------------------------------------------- +# Project file helpers +# --------------------------------------------------------------------------- +projectHasMakefile(){ + [[ -f "${1}/Makefile" ]] +} + +ensureProjectMakefile(){ + local project="$1" + local template="" + [[ -f "${project}/Makefile" ]] && return 0 + + case "$OF_PLATFORM" in + osx|macos) + if [[ -f "${OF_DIR}/scripts/templates/macos/Makefile" ]]; then + template="${OF_DIR}/scripts/templates/macos" + else + template="${OF_DIR}/scripts/templates/osx" + fi + ;; + linux) + case "$OF_ARCH" in + x86_64|amd64) template="${OF_DIR}/scripts/templates/linux64" ;; + *) template="${OF_DIR}/scripts/templates/linux" ;; + esac + ;; + msys2) template="${OF_DIR}/scripts/templates/msys2" ;; + *) template="${OF_DIR}/scripts/templates/osx" ;; + esac + + if [[ -f "${template}/Makefile" ]]; then + echoNote "copying Makefile from $(basename "$template") template" + cp "${template}/Makefile" "${project}/Makefile" + [[ ! -f "${project}/config.make" && -f "${template}/config.make" ]] && \ + cp "${template}/config.make" "${project}/config.make" + return 0 + fi + return 1 +} + +runProjectGenerator(){ + local project="$1" + shift + local platforms="${1:-}" + local bin + bin=$(findPGBinary) || { + echoError "Project Generator not found — run: of update pg" + return 1 + } + local -a cmd=( "$bin" -o"$OF_DIR" ) + if [[ -n "$platforms" ]]; then + cmd+=( -p"$platforms" ) + fi + cmd+=( "$project" ) + echoNote "${cmd[*]}" + PG_OF_PATH="$OF_DIR" "${cmd[@]}" +} + +# --------------------------------------------------------------------------- +# Build runners +# --------------------------------------------------------------------------- +runMakeInProject(){ + local project="$1" + local config="${2:-Release}" + local jobs + jobs=$(ofBuildJobs) + ensureProjectMakefile "$project" || { + echoError "no Makefile and could not install template Makefile" + echoNote "try: of build generate \"$project\"" + return 1 + } + echoInfo "make ${config} -j${jobs} · $(basename "$project")" + echoKV "dir" "$project" + ( + cd "$project" || exit 1 + make -j"$jobs" "$config" + ) +} + +runMakeCore(){ + local config="${1:-Release}" + local jobs + jobs=$(ofBuildJobs) + local coreDir="${OF_DIR}/libs/openFrameworksCompiled/project" + echoInfo "OF core make ${config} -j${jobs}" + echoKV "dir" "$coreDir" + ( + cd "$coreDir" || exit 1 + make -j"$jobs" "$config" + ) +} + +runXcodeBuild(){ + local project="$1" + local config="${2:-Release}" + local xproj name + xproj=$(compgen -G "${project}/*.xcodeproj" | head -1) + [[ -n "$xproj" && -d "$xproj" ]] || { + echoError "no .xcodeproj in $project" + echoNote "run PG first: of build generate \"$project\"" + return 1 + } + name=$(basename "$xproj" .xcodeproj) + echoInfo "xcodebuild ${config} · ${name}" + ( + cd "$project" || exit 1 + xcodebuild -configuration "$config" -project "$(basename "$xproj")" -target "$name" \ + -jobs "$(ofBuildJobs)" + ) +} + +runEmscriptenBuild(){ + local project="$1" + local config="${2:-Debug}" + local jobs debugFlag=0 + jobs=$(ofBuildJobs) + + ensureEmscriptenEnv || { + echoError "emmake/emcc not found" + echoNote "install: scripts/emscripten/install_emscripten.sh or source ~/emsdk/emsdk_env.sh" + return 1 + } + + # Ensure Makefile exists (host template is fine; emmake sets CC=emcc → PLATFORM_OS=emscripten) + ensureProjectMakefile "$project" || { + echoError "need Makefile in project" + return 1 + } + + [[ "$config" == "Debug" ]] && debugFlag=1 + echoInfo "emscripten emmake make ${config} -j${jobs}" + echoKV "emcc" "$(command -v emcc)" + echoKV "dir" "$project" + ( + cd "$project" || exit 1 + export EMCC_DEBUG="${EMCC_DEBUG:-$debugFlag}" + emmake make -j"$jobs" "$config" + ) +} + +findEmscriptenIndexHtml(){ + local project="$1" + local f + # Preferred layout: bin/em//index.html + f=$(find "${project}/bin/em" -name 'index.html' 2>/dev/null | head -1) + [[ -n "$f" ]] && { printf '%s' "$f"; return 0; } + f=$(find "${project}/bin" -name 'index.html' 2>/dev/null | head -1) + [[ -n "$f" ]] && { printf '%s' "$f"; return 0; } + return 1 +} + +runEmscriptenOpen(){ + local project="$1" + local html browser="" + html=$(findEmscriptenIndexHtml "$project") || { + echoError "no emscripten index.html under ${project}/bin" + echoNote "build first with: of build emscripten " + return 1 + } + echoKV "html" "$html" + + if ! command -v emrun >/dev/null 2>&1; then + ensureEmscriptenEnv || true + fi + + browser=$(findChromeForEmrun || true) + if command -v emrun >/dev/null 2>&1; then + local -a args=( --no_emrun_detect ) + if [[ -n "$browser" ]]; then + args+=( --browser "$browser" ) + echoInfo "emrun → ${browser}" + else + echoInfo "emrun (default browser)" + fi + # enable-unsafe-webgpu is useful for some OF webgpu experiments; ignore if unsupported + ( + cd "$(dirname "$html")" || exit 1 + emrun "${args[@]}" --enable-unsafe-webgpu "$(basename "$html")" 2>/dev/null \ + || emrun "${args[@]}" "$(basename "$html")" + ) + return $? + fi + + # Fallback: open file / serve not available — use system open + if [[ -n "$browser" && "$(uname -s)" == "Darwin" ]]; then + echoInfo "open in Google Chrome (emrun missing)" + open -a "Google Chrome" "$html" + return $? + fi + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$html" + return $? + fi + if command -v open >/dev/null 2>&1; then + open "$html" + return $? + fi + echoError "no emrun / open available; open manually: $html" + return 1 +} + +runCmakeProject(){ + local project="$1" + local config="${2:-Release}" + local buildDir="${project}/build" + local jobs + jobs=$(ofBuildJobs) + + command -v cmake >/dev/null 2>&1 || { + echoError "cmake not found" + return 1 + } + + if [[ ! -f "${project}/CMakeLists.txt" ]]; then + echoError "no CMakeLists.txt in $(basename "$project")" + echoNote "desktop oF projects normally use make/xcode; CMake is first-class for Android" + echoNote "toolchains live in libs/openFrameworksCompiled/project/CMake/toolchain/" + return 1 + fi + + echoInfo "cmake · ${config} · $(basename "$project")" + mkdir -p "$buildDir" + ( + cmake -S "$project" -B "$buildDir" \ + -DCMAKE_BUILD_TYPE="$config" \ + ${OF_CMAKE_ARGS:-} \ + && cmake --build "$buildDir" --config "$config" -j"$jobs" + ) +} + +runCmakeAndroidCore(){ + local androidDir="${OF_DIR}/libs/openFrameworksCompiled/project/android/openframeworksAndroid" + local config="${1:-Debug}" + [[ -f "${androidDir}/CMakeLists.txt" ]] || { + echoError "Android CMakeLists not found at openframeworksAndroid" + return 1 + } + echoWarning "Android NDK CMake usually runs via Gradle/Android Studio" + echoKV "dir" "$androidDir" + if [[ -f "${androidDir}/gradlew" ]]; then + confirmYes "Run ./gradlew assemble${config} in openframeworksAndroid?" || return 0 + ( + cd "$androidDir" || exit 1 + ./gradlew "assemble${config}" + ) + return $? + fi + runCmakeProject "$androidDir" "$config" +} + +runCmakeStatus(){ + printBanner "cmake" + printf '\n' + if command -v cmake >/dev/null 2>&1; then + echoKV "cmake" "$(cmake --version 2>/dev/null | head -1)" + else + echoKV "cmake" "not installed" + fi + echoKV "toolchains" "${OF_DIR}/libs/openFrameworksCompiled/project/CMake/toolchain" + local t + for t in "${OF_DIR}/libs/openFrameworksCompiled/project/CMake/toolchain"/*.cmake; do + [[ -f "$t" ]] && echoNote " $(basename "$t")" + done + local androidCm="${OF_DIR}/libs/openFrameworksCompiled/project/android/openframeworksAndroid/CMakeLists.txt" + if [[ -f "$androidCm" ]]; then + echoKV "android OF" "CMakeLists.txt present (Gradle/NDK)" + else + echoKV "android OF" "—" + fi + echoKV "desktop OF" "make / Xcode / VS are primary" + echoNote "per-project cmake works when CMakeLists.txt exists (Android templates, some apps)" + echoNote "set OF_CMAKE_ARGS for extra configure flags" + printf '\n' +} + +runCleanProject(){ + local project="$1" + if projectHasMakefile "$project" || ensureProjectMakefile "$project" 2>/dev/null; then + echoInfo "make clean · $(basename "$project")" + ( cd "$project" && make clean ) + return $? + fi + if [[ -d "${project}/build" ]]; then + echoInfo "rm -rf build/" + rm -rf "${project}/build" + return 0 + fi + echoError "nothing to clean" + return 1 +} + +runNativeApp(){ + local project="$1" + local config="${2:-Release}" + local name app + name=$(basename "$project") + if [[ "$config" == "Debug" ]]; then + app="${project}/bin/${name}_debug.app" + [[ -d "$app" ]] || app="${project}/bin/${name}_debug" + else + app="${project}/bin/${name}.app" + [[ -d "$app" ]] || app="${project}/bin/${name}" + fi + if [[ -d "$app" && "$(uname -s)" == "Darwin" ]]; then + echoInfo "open $(basename "$app")" + open "$app" + return $? + fi + if [[ -x "$app" ]]; then + echoInfo "run $app" + "$app" & + return 0 + fi + # macOS sometimes uses bin/name.app only for Release + app=$(find "${project}/bin" -maxdepth 2 -name '*.app' 2>/dev/null | head -1) + if [[ -n "$app" ]]; then + open "$app" + return $? + fi + echoWarning "could not locate built app under bin/" + return 1 +} + +# --------------------------------------------------------------------------- +# High-level command: build one project with a system +# --------------------------------------------------------------------------- +cmdBuildProject(){ + local project="$1" + local system="${2:-make}" + local config="${3:-Release}" + local openAfter="${4:-0}" + + project=$(resolveProjectPath "$project") || { + echoError "project not found: $1" + return 1 + } + isOfProjectDir "$project" || echoWarning "dir may not be a full OF project: $project" + + printBanner "build" + echoKV "project" "$project" + echoKV "system" "$system" + echoKV "config" "$config" + printf '\n' + + case "$system" in + make|host) + runMakeInProject "$project" "$config" || return $? + [[ "$openAfter" == "1" ]] && runNativeApp "$project" "$config" + ;; + xcode|xcodebuild) + runXcodeBuild "$project" "$config" || return $? + [[ "$openAfter" == "1" ]] && runNativeApp "$project" "$config" + ;; + emscripten|em|wasm) + runEmscriptenBuild "$project" "$config" || return $? + if [[ "$openAfter" == "1" ]]; then + runEmscriptenOpen "$project" + elif menuCanRun; then + if confirmYes "Open in browser (Chrome if available)?"; then + runEmscriptenOpen "$project" + fi + fi + ;; + cmake) + runCmakeProject "$project" "$config" || return $? + ;; + generate|pg) + local plats + plats=$(ofHostPgPlatform) + runProjectGenerator "$project" "$plats" || return $? + ;; + clean) + runCleanProject "$project" || return $? + ;; + *) + echoError "unknown build system: $system" + echoNote "valid: make xcode emscripten cmake generate clean" + return 1 + ;; + esac + echoSuccess "done · $(basename "$project")" +} + +cmdBuildCore(){ + local config="${1:-Release}" + printBanner "build" + echoInfo "openFrameworks core library" + runMakeCore "$config" || return $? + echoSuccess "core ${config} built" +} + +cmdBuildGenerate(){ + local project="$1" + local platforms="${2:-}" + project=$(resolveProjectPath "$project") || { + echoError "path not found: $1" + return 1 + } + [[ -n "$platforms" ]] || platforms=$(ofHostPgPlatform) + printBanner "generate" + echoKV "project" "$project" + echoKV "platforms" "$platforms" + runProjectGenerator "$project" "$platforms" || return $? + echoSuccess "project files updated" +} + +# --------------------------------------------------------------------------- +# Interactive Build menu +# --------------------------------------------------------------------------- +menuBuildProjectActions(){ + local project="$1" + local system config openAfter=0 + + echoKV "project" "$project" + printf '\n' + menuPickBuildSystem "$project" || return 0 + system="$UI_MENU_RESULT" + + if [[ "$system" == "generate" ]]; then + local -a popts=() + local p plats="" + for p in osx macos emscripten ios linux64 linux vs msys2 android tvos; do + popts+=("${p}|${p}") + done + if menuCanRun; then + if menuPickMulti "PG platforms (space-separated multi-select)" "${popts[@]}"; then + # ids are space-separated → comma list for projectGenerator + plats=$(echo "$UI_MENU_RESULT" | tr -s '[:space:]' ',' | sed 's/^,//;s/,$//') + fi + fi + [[ -z "$plats" ]] && plats=$(ofHostPgPlatform) + cmdBuildGenerate "$project" "$plats" + return $? + fi + + menuPickConfig || return 0 + config="$UI_MENU_RESULT" + + case "$system" in + emscripten) + cmdBuildProject "$project" emscripten "$config" 0 + local st=$? + [[ $st -ne 0 ]] && return $st + if confirmYes "Open build in browser (Chrome if installed)?"; then + runEmscriptenOpen "$project" + fi + ;; + make|xcode|cmake) + cmdBuildProject "$project" "$system" "$config" 0 + local st=$? + [[ $st -ne 0 ]] && return $st + if [[ "$system" != "cmake" ]] && confirmNo "Run / open the app?"; then + runNativeApp "$project" "$config" + fi + ;; + *) + cmdBuildProject "$project" "$system" "$config" 0 + ;; + esac +} + +menuCmake(){ + local choice + printBanner "cmake" + runCmakeStatus + menuPick "CMake actions" \ + "Status (again)|status" \ + "Build project with CMakeLists.txt…|project" \ + "Android OF core (Gradle/CMake)…|android" \ + "Back|back" \ + || return 0 + choice="$UI_MENU_RESULT" + case "$choice" in + status) runCmakeStatus ;; + project) + local path + menuPick "Where is the CMake project?" \ + "Browse apps…|apps" \ + "Browse examples…|examples" \ + "Custom path…|path" \ + "Back|back" || return 0 + case "$UI_MENU_RESULT" in + apps) menuPickAppProject || return 0; path="$UI_MENU_RESULT" ;; + examples) menuPickExample || return 0; path="$UI_MENU_RESULT" ;; + path) + menuInput "Path to project with CMakeLists.txt" || return 0 + path=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not found"; return 1 + } + ;; + *) return 0 ;; + esac + menuPickConfig || return 0 + runCmakeProject "$path" "$UI_MENU_RESULT" + ;; + android) + menuPickConfig || return 0 + runCmakeAndroidCore "$UI_MENU_RESULT" + ;; + back) return 0 ;; + esac +} + +menuBuild(){ + local choice project + if ! menuCanRun; then + echoWarning "no TTY — use: of build core|project|example|emscripten|generate|cmake" + printHelpBuild + return 0 + fi + + while true; do + printf '\n' + printBanner "build" + echoKV "host" "${OF_PLATFORM}${OF_ARCH:+ / ${OF_ARCH}}" + echoKV "jobs" "$(ofBuildJobs) (OF_JOBS to override)" + local em="—" + if ensureEmscriptenEnv >/dev/null 2>&1; then em="emcc ready"; else em="not in PATH"; fi + echoKV "emscripten" "$em" + local pg + pg=$(findPGBinary 2>/dev/null) || pg="" + echoKV "project gen" "${pg:-not installed}" + echoKV "cmake" "$(command -v cmake >/dev/null 2>&1 && cmake --version 2>/dev/null | head -1 || echo 'not installed')" + printf '\n' + + menuPick "Build…" \ + "OF core library (make)|core" \ + "Project under apps/…|apps" \ + "Example under examples/…|examples" \ + "Addon example…|addons" \ + "Custom project path…|path" \ + "Emscripten — pick project, build, open browser…|emscripten" \ + "Generate project files (PG)…|generate" \ + "CMake…|cmake" \ + "Clean project…|clean" \ + "Help|help" \ + "Back|back" \ + || return 0 + choice="$UI_MENU_RESULT" + printf '\n' + + case "$choice" in + core) + menuPickConfig || continue + cmdBuildCore "$UI_MENU_RESULT" + menuPause + ;; + apps) + menuPickAppProject || continue + menuBuildProjectActions "$UI_MENU_RESULT" + menuPause + ;; + examples) + menuPickExample || continue + menuBuildProjectActions "$UI_MENU_RESULT" + menuPause + ;; + addons) + menuPickAddonExample || continue + menuBuildProjectActions "$UI_MENU_RESULT" + menuPause + ;; + path) + menuInput "Project path" "apps/myApps/" || continue + project=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not found: $UI_INPUT_RESULT" + menuPause + continue + } + menuBuildProjectActions "$project" + menuPause + ;; + emscripten) + menuPick "Emscripten project from…" \ + "apps/…|apps" \ + "examples/…|examples" \ + "addon example…|addons" \ + "custom path…|path" \ + "Back|back" || continue + case "$UI_MENU_RESULT" in + apps) menuPickAppProject || continue; project="$UI_MENU_RESULT" ;; + examples) menuPickExample || continue; project="$UI_MENU_RESULT" ;; + addons) menuPickAddonExample || continue; project="$UI_MENU_RESULT" ;; + path) + menuInput "Project path" || continue + project=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not found"; menuPause; continue + } + ;; + *) continue ;; + esac + menuPickConfig || continue + cmdBuildProject "$project" emscripten "$UI_MENU_RESULT" 0 + if confirmYes "Open in browser (Chrome if available)?"; then + runEmscriptenOpen "$project" + fi + menuPause + ;; + generate) + menuPick "Generate files for…" \ + "apps/…|apps" \ + "examples/…|examples" \ + "addon example…|addons" \ + "custom path…|path" \ + "recursive examples/|rec-examples" \ + "Back|back" || continue + case "$UI_MENU_RESULT" in + apps) menuPickAppProject || continue; project="$UI_MENU_RESULT" ;; + examples) menuPickExample || continue; project="$UI_MENU_RESULT" ;; + addons) menuPickAddonExample || continue; project="$UI_MENU_RESULT" ;; + path) + menuInput "Project path" || continue + project=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not found"; menuPause; continue + } + ;; + rec-examples) + local bin plats + bin=$(findPGBinary) || { + echoError "PG missing — of update pg" + menuPause + continue + } + plats=$(ofHostPgPlatform) + confirmYes "Recursively update all examples for ${plats}?" || continue + echoNote "$bin -r -o${OF_DIR} -p${plats} ${OF_DIR}/examples" + PG_OF_PATH="$OF_DIR" "$bin" -r -o"$OF_DIR" -p"$plats" "${OF_DIR}/examples" + menuPause + continue + ;; + *) continue ;; + esac + local plats + plats=$(ofHostPgPlatform) + if confirmYes "Also add emscripten platform files?"; then + plats="${plats},emscripten" + fi + cmdBuildGenerate "$project" "$plats" + menuPause + ;; + cmake) + menuCmake + menuPause + ;; + clean) + menuPick "Clean…" \ + "apps/…|apps" \ + "examples/…|examples" \ + "custom path…|path" \ + "Back|back" || continue + case "$UI_MENU_RESULT" in + apps) menuPickAppProject || continue; project="$UI_MENU_RESULT" ;; + examples) menuPickExample || continue; project="$UI_MENU_RESULT" ;; + path) + menuInput "Project path" || continue + project=$(resolveProjectPath "$UI_INPUT_RESULT") || { + echoError "not found"; menuPause; continue + } + ;; + *) continue ;; + esac + runCleanProject "$project" + menuPause + ;; + help) + printHelpBuild + menuPause + ;; + back) return 0 ;; + esac + done +} + +printHelpBuild(){ + local prog + prog=$(basename "${0:-of}") + cat << EOF + + Build commands + ${prog} build Interactive build menu (TTY) + ${prog} build core [Debug|Release] Build OF compiled core (make) + ${prog} build project [sys] [cfg] + ${prog} build example [sys] [cfg] + ${prog} build emscripten [Debug|Release] + ${prog} build generate [platforms] + ${prog} build cmake [Debug|Release] + ${prog} build cmake status + ${prog} build clean + ${prog} build open-em Open emscripten index.html (Chrome/emrun) + + Systems (project) + make (default) xcode emscripten cmake generate + + Env + OF_JOBS=8 Parallel jobs + OF_CMAKE_ARGS="..." Extra cmake -S/-B flags + EMCC_DEBUG=1 Emscripten debug logging + + Examples + ${prog} build core Debug + ${prog} build project apps/myApps/mySketch + ${prog} build project examples/graphics/graphicsExample xcode Debug + ${prog} build emscripten examples/graphics/graphicsExample Debug + ${prog} build generate apps/myApps/mySketch osx,emscripten + +EOF +} diff --git a/scripts/osx/download_libs.sh b/scripts/osx/download_libs.sh index 8a693de45d4..ee04ce524d1 100755 --- a/scripts/osx/download_libs.sh +++ b/scripts/osx/download_libs.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash +# osx host package; Apple multi-target xcframeworks prefer scripts/macos/download_libs.sh +# Platform-clean maps osx → lib/macos so sibling platforms are preserved. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" if [ ! -z ${BITS+x} ]; then - ../dev/download_libs.sh -p osx -b -a $BITS $@ + ../dev/download_libs.sh -p osx -b -a $BITS "$@" else - ../dev/download_libs.sh -p osx -b $@ + ../dev/download_libs.sh -p osx -b "$@" fi diff --git a/scripts/ui.sh b/scripts/ui.sh index 2a2bb86d045..9b481a58b84 100755 --- a/scripts/ui.sh +++ b/scripts/ui.sh @@ -126,6 +126,25 @@ confirmNo(){ [[ "$confirm" =~ ^[Yy]$ ]] } +# Free-text input. Sets UI_INPUT_RESULT. Returns 1 on empty/cancel. +# Usage: menuInput "Prompt" ["default"] +menuInput(){ + local prompt="$1" + local default="${2:-}" + UI_INPUT_RESULT="" + if [[ "$UI_HAS_GUM" -eq 1 ]] && menuCanRun; then + UI_INPUT_RESULT=$(gum input --prompt "> " --placeholder "$prompt" --value "$default" 2>/dev/null) || return 1 + else + if [[ -n "$default" ]]; then + read -r -p "$(printf '%s›%s %s [%s]: ' "$C_ACCENT" "$C_RESET" "$prompt" "$default")" UI_INPUT_RESULT + [[ -z "$UI_INPUT_RESULT" ]] && UI_INPUT_RESULT="$default" + else + read -r -p "$(printf '%s›%s %s: ' "$C_ACCENT" "$C_RESET" "$prompt")" UI_INPUT_RESULT + fi + fi + [[ -n "$UI_INPUT_RESULT" ]] +} + tasksReset(){ UI_TASK_LABELS=() UI_TASK_STATUS=() diff --git a/scripts/vs/download_libs.sh b/scripts/vs/download_libs.sh index e842fae9755..4b0e298970e 100755 --- a/scripts/vs/download_libs.sh +++ b/scripts/vs/download_libs.sh @@ -1,8 +1,39 @@ #!/usr/bin/env bash +# Platform-clean removes only lib/vs; shared include/bin need --full-clean. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $SCRIPT_DIR +cd "$SCRIPT_DIR" + +detectHostVsArch(){ + local m pa + pa="${PROCESSOR_ARCHITECTURE:-}" + [[ -n "${PROCESSOR_ARCHITEW6432:-}" ]] && pa="${PROCESSOR_ARCHITEW6432}" + case "${MSYSTEM:-}" in + CLANGARM64|clangarm64) echo "arm64"; return 0 ;; + esac + case "$(echo "$pa" | tr '[:lower:]' '[:upper:]')" in + ARM64) echo "arm64"; return 0 ;; + AMD64|X86) echo "64"; return 0 ;; + esac + m=$(uname -m 2>/dev/null || echo "") + case "$m" in + aarch64|arm64|ARM64) echo "arm64"; return 0 ;; + *) echo "64"; return 0 ;; + esac +} + if [ ! -z ${BITS+x} ]; then - ../dev/download_libs.sh -p vs -b -a $BITS -n $@ + ../dev/download_libs.sh -p vs -b -a "$BITS" "$@" else - ../dev/download_libs.sh -p vs -b -n $@ -fi + # Pass through -a if user already provided it + has_a=0 + for arg in "$@"; do + case "$arg" in -a|--arch) has_a=1; break ;; esac + done + if [[ $has_a -eq 1 ]]; then + ../dev/download_libs.sh -p vs -b "$@" + else + HOST_VS=$(detectHostVsArch) + echo " VS download · host arch → ${HOST_VS}" + ../dev/download_libs.sh -p vs -b -a "$HOST_VS" "$@" + fi +fi