From 33c9126b42ef35c1b91b60cd1c133c216c25a6fa Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Thu, 6 Aug 2026 01:38:39 +1000 Subject: [PATCH 01/13] of Menu 4.0 --- scripts/of.sh | 101 ++++- scripts/of_build.sh | 1030 +++++++++++++++++++++++++++++++++++++++++++ scripts/ui.sh | 19 + 3 files changed, 1147 insertions(+), 3 deletions(-) create mode 100644 scripts/of_build.sh diff --git a/scripts/of.sh b/scripts/of.sh index 09f60624650..27dea810ac0 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:-}" @@ -708,16 +709,18 @@ 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) 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" 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 +728,15 @@ 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} 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' } @@ -1464,6 +1471,7 @@ cmdMenu(){ "Status — system checker|status" \ "${setupLabel}|setup" \ "Update — libs / PG (choose source)|update" \ + "Build… — core / projects / examples / emscripten / cmake|build" \ "Build libraries (Apothecary)…|apothecary" \ "Show openFrameworks version|version" \ "Show Project Generator version|version-pg" \ @@ -1481,6 +1489,7 @@ cmdMenu(){ status) cmdStatus; menuPause ;; setup) cmdSetup "$OF_PLATFORM"; menuPause ;; update) menuUpdate; menuPause ;; + build) menuBuild ;; apothecary) menuApothecary; menuPause ;; version) cmdVersion; menuPause ;; version-pg) cmdVersionPG; menuPause ;; @@ -1524,6 +1533,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 +1623,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 ;; @@ -1550,7 +1645,7 @@ runCommand(){ ;; *) echoError "Unknown command: $cmd" - echoNote "valid: menu status setup update version upgrade apothecary help" + echoNote "valid: menu status setup update build 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/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=() From bf8cc5a5da3f69a991d330eb03534482a1da80cf Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Thu, 6 Aug 2026 13:14:37 +1000 Subject: [PATCH 02/13] download_libs [2.7.1] - > [2.7.2] - allow for other platforms --- scripts/android/download_libs.sh | 5 +- scripts/dev/download_libs.sh | 127 +++++++++++++++++++--------- scripts/dev/lib_sources.sh | 31 +++++-- scripts/emscripten/download_libs.sh | 8 +- scripts/ios/download_libs.sh | 9 +- scripts/linux/download_libs.sh | 5 +- scripts/macos/download_libs.sh | 9 +- scripts/msys2/download_libs.sh | 5 +- scripts/of.sh | 15 ++++ scripts/osx/download_libs.sh | 9 +- scripts/vs/download_libs.sh | 10 ++- 11 files changed, 162 insertions(+), 71 deletions(-) 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..72d5023c317 100755 --- a/scripts/dev/download_libs.sh +++ b/scripts/dev/download_libs.sh @@ -4,11 +4,12 @@ VER=latest PLATFORM="" ARCH="" OVERWRITE=1 +FULL_CLEAN=0 LEGACY=0 SILENT_ARGS="" NO_SSL="" BLEEDING_EDGE=0 -DL_VERSION=2.7.1 +DL_VERSION=2.7.2 TAG="" printHelp(){ @@ -25,8 +26,12 @@ 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. -s, --silent Silent download progress -h, --help Shows this message -k, --no-ssl Allow no SSL validation @@ -107,6 +112,10 @@ while [[ $# -gt 0 ]]; do -n|--no-overwrite) OVERWRITE=0 ;; + --full-clean) + FULL_CLEAN=1 + OVERWRITE=1 + ;; -b|--bleeding-edge) BLEEDING_EDGE=1 ;; @@ -350,31 +359,49 @@ download "${PKGS[@]}" 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 +412,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 +493,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 diff --git a/scripts/dev/lib_sources.sh b/scripts/dev/lib_sources.sh index 1da37a71a53..d2ddb3ac8c4 100755 --- a/scripts/dev/lib_sources.sh +++ b/scripts/dev/lib_sources.sh @@ -133,6 +133,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[@]}" } @@ -188,18 +194,33 @@ downloadOfLibs(){ 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})" + echo " ofLibs install complete ok=${ok} skipped=${skip} (libs//{include,lib/})" echo " note: ofLibs is ofWorks-oriented; stock oF may need path tweaks for some libs." [[ $ok -gt 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 27dea810ac0..62db7c01409 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -719,6 +719,7 @@ printHelp(){ 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 @@ -1152,6 +1153,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; } 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/vs/download_libs.sh b/scripts/vs/download_libs.sh index e842fae9755..02abf2062c2 100755 --- a/scripts/vs/download_libs.sh +++ b/scripts/vs/download_libs.sh @@ -1,8 +1,10 @@ #!/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" 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 + ../dev/download_libs.sh -p vs -b "$@" +fi + From 1b0826219101d1a494939e9cbb5623c85e3046a2 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Fri, 7 Aug 2026 22:50:01 +1000 Subject: [PATCH 03/13] ofMenu updates - Clean - SHA 256 verify --- scripts/dev/download_libs.sh | 191 +++++++++---- scripts/dev/lib_sources.sh | 105 ++++++- scripts/dev/sha_verify.sh | 309 +++++++++++++++++++++ scripts/of.sh | 524 ++++++++++++++++++++++++++++++++++- scripts/vs/download_libs.sh | 35 ++- 5 files changed, 1097 insertions(+), 67 deletions(-) create mode 100644 scripts/dev/sha_verify.sh diff --git a/scripts/dev/download_libs.sh b/scripts/dev/download_libs.sh index 72d5023c317..495024a843f 100755 --- a/scripts/dev/download_libs.sh +++ b/scripts/dev/download_libs.sh @@ -5,12 +5,22 @@ 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.2 +DL_VERSION=2.7.4 TAG="" +REPO="latest" +SHA_VERIFIED=0 +SHA_SKIPPED=0 +SHA_FAILED=0 +SHA_STATUS="unchecked" printHelp(){ cat << EOF @@ -32,6 +42,8 @@ cat << EOF --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 @@ -43,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 " @@ -116,6 +152,13 @@ while [[ $# -gt 0 ]]; do FULL_CLEAN=1 OVERWRITE=1 ;; + --skip-sha) + VERIFY_SHA=0 + ;; + --require-sha) + REQUIRE_SHA=1 + VERIFY_SHA=1 + ;; -b|--bleeding-edge) BLEEDING_EDGE=1 ;; @@ -170,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) @@ -212,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 @@ -219,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" @@ -255,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" + 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 - VS_PLATFORM="${PLATFORM}" + 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 - if [[ $LEGACY == 1 ]]; then - PKGS="openFrameworksLibs_${VER}_${PLATFORM}_2019_64_1.zip \ - openFrameworksLibs_${VER}_${PLATFORM}_2019_64_2.zip" - else - PKGS="openFrameworksLibs_${VER}_${VS_PLATFORM}_${ARCH}_1.zip \ - openFrameworksLibs_${VER}_${VS_PLATFORM}_${ARCH}_2.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 @@ -357,6 +441,13 @@ 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") @@ -567,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 d2ddb3ac8c4..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}" @@ -150,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 @@ -177,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 @@ -191,6 +212,30 @@ 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" @@ -220,9 +265,25 @@ downloadOfLibs(){ [[ -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 @@ -258,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/of.sh b/scripts/of.sh index 62db7c01409..2bd602a9298 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -104,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 @@ -111,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 @@ -119,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 } @@ -294,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 } @@ -309,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}" } @@ -656,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[@]}" @@ -710,6 +826,7 @@ printHelp(){ 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 @@ -732,6 +849,9 @@ printHelp(){ ${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 @@ -773,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} @@ -785,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 } @@ -833,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" @@ -1182,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 @@ -1463,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 @@ -1488,6 +1953,7 @@ cmdMenu(){ "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" \ @@ -1506,6 +1972,7 @@ cmdMenu(){ update) menuUpdate; menuPause ;; build) menuBuild ;; apothecary) menuApothecary; menuPause ;; + cleanup|clean) menuCleanup ;; version) cmdVersion; menuPause ;; version-pg) cmdVersionPG; menuPause ;; upgrade-addons) cmdUpgrade addons; menuPause ;; @@ -1650,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 ;; @@ -1660,7 +2156,7 @@ runCommand(){ ;; *) echoError "Unknown command: $cmd" - echoNote "valid: menu status setup update build version upgrade apothecary help" + echoNote "valid: menu status setup update build cleanup version upgrade apothecary help" printHelp return 1 ;; diff --git a/scripts/vs/download_libs.sh b/scripts/vs/download_libs.sh index 02abf2062c2..4b0e298970e 100755 --- a/scripts/vs/download_libs.sh +++ b/scripts/vs/download_libs.sh @@ -2,9 +2,38 @@ # Platform-clean removes only lib/vs; shared include/bin need --full-clean. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 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 "$@" + ../dev/download_libs.sh -p vs -b -a "$BITS" "$@" else - ../dev/download_libs.sh -p vs -b "$@" + # 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 - From 95a30412bc52a8b6675a859f16f379269e3346ad Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Sun, 9 Aug 2026 18:29:00 +1000 Subject: [PATCH 04/13] Fix menu on windows --- scripts/ui.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/ui.sh b/scripts/ui.sh index 9b481a58b84..6f127a30dc6 100755 --- a/scripts/ui.sh +++ b/scripts/ui.sh @@ -106,6 +106,7 @@ confirmYes(){ return $? fi read -r -p "$(printf '%s?%s %s [Y/n]: ' "$C_WARN" "$C_RESET" "$prompt")" confirm + confirm="${confirm%$'\r'}" [[ -z "$confirm" || "$confirm" =~ ^[Yy]$ ]] } @@ -383,8 +384,15 @@ menuPick(){ printf ' %s────────────────────────────────────────%s\n' "$C_MUTED" "$C_RESET" printf ' %s›%s choose [1-%d, q]: ' "$C_ACCENT" "$C_RESET" "${#labels[@]}" read -r choice - [[ -z "$choice" || "$choice" =~ ^[Qq]$ ]] && return 1 - if [[ ! "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#labels[@]} )); then + [[ -z "$choice" || "$choice" =~ ^[[:space:]]*[Qq] ]] && return 1 + # tolerate stray CR / whitespace / trailing junk (e.g. Windows consoles): use the first number typed + if [[ "$choice" =~ [0-9]+ ]]; then + choice="${BASH_REMATCH[0]}" + else + echoError "invalid choice: ${choice}" + return 1 + fi + if (( choice < 1 || choice > ${#labels[@]} )); then echoError "invalid choice: ${choice}" return 1 fi @@ -447,14 +455,15 @@ menuPickMulti(){ printf ' %s────────────────────────────────────────%s\n' "$C_MUTED" "$C_RESET" printf ' %s›%s multi-select [e.g. 1 3 5, a=all, q=cancel]: ' "$C_ACCENT" "$C_RESET" read -r choice - [[ -z "$choice" || "$choice" =~ ^[Qq]$ ]] && return 1 - if [[ "$choice" =~ ^[Aa]$ ]]; then + [[ -z "$choice" || "$choice" =~ ^[[:space:]]*[Qq] ]] && return 1 + if [[ "$choice" =~ ^[[:space:]]*[Aa] ]]; then UI_MENU_RESULT="${ids[*]}" return 0 fi + # tolerate stray CR / commas / trailing junk per token: use the first number in each for n in $choice; do - n=${n//,/} - [[ "$n" =~ ^[0-9]+$ ]] || continue + [[ "$n" =~ [0-9]+ ]] || continue + n="${BASH_REMATCH[0]}" if (( n >= 1 && n <= ${#ids[@]} )); then selected+=("${ids[$((n - 1))]}") fi From 3054a8c18987c39778fc6970331b6ca1f0574fd8 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 00:56:45 +1000 Subject: [PATCH 05/13] oF Menu updates - Fixed clean - display MB freed, libs instant removed, examples checked if built before clean, project cleans safer (project cleans to trash/recycle, no bin/ removed) - Fixed VS builds / Windows menu / projectGenerator opened - added osx install_dependencies.sh (for Xcode select) --- scripts/dev/upgrade.sh | 15 +- scripts/of.sh | 426 ++++++++++++++++++++-------- scripts/of_build.sh | 259 ++++++++++++++++- scripts/osx/install_dependencies.sh | 65 +++++ 4 files changed, 632 insertions(+), 133 deletions(-) create mode 100755 scripts/osx/install_dependencies.sh diff --git a/scripts/dev/upgrade.sh b/scripts/dev/upgrade.sh index 788fa5fcf44..308ef690007 100755 --- a/scripts/dev/upgrade.sh +++ b/scripts/dev/upgrade.sh @@ -1,7 +1,7 @@ #!/bin/bash UP_VERSION=0.0.1 -local BASE_DIR=$1 +BASE_DIR="$1" OF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" OF_DIR="$(realpath "$OF_DIR/../../")" @@ -9,8 +9,17 @@ OF_CORE_SCRIPT_DIR="$(realpath "$OF_DIR/scripts")" OF_CORE_CI_SCRIPT_DIR="$(realpath "$OF_DIR/scripts/ci")" OF_PG_INSTALLED_DIR="$(realpath "$OF_DIR/projectGenerator")" -read -p "Enter the base directory to search for addon_config.mk files: " BASE_DIR -if [[ ! -d "${OF_DIR}/$BASE_DIR" ]]; then +if [[ -n "$BASE_DIR" ]]; then + read -p "Enter the base directory to search for addon_config.mk files [${BASE_DIR}]: " INPUT_DIR + [[ -n "$INPUT_DIR" ]] && BASE_DIR="$INPUT_DIR" +else + read -p "Enter the base directory to search for addon_config.mk files: " BASE_DIR +fi + +if [[ "$BASE_DIR" != /* ]]; then + BASE_DIR="${OF_DIR}/${BASE_DIR}" +fi +if [[ ! -d "$BASE_DIR" ]]; then echo "Error: Directory $BASE_DIR does not exist." exit 1 fi diff --git a/scripts/of.sh b/scripts/of.sh index 2bd602a9298..c7f0bff57f7 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -525,14 +525,23 @@ readLibVersionMeta(){ return 1 } -# Inline list: libpng (1.6.58), glfw (3.4), assimp (5.4.3 · ofxAssimpModelLoader), … -# Sets FORMATTED_LIB_LINE and FORMATTED_LIB_COUNT -formatLibInlineList(){ - local -a items=("$@") - local path name note entry line="" n=0 +# Streams "name (version · note)" one line at a time as each library is read, +# instead of resolving the whole section silently before printing anything. +# Version lookup does several `find` passes per lib (pkl/pkgconfig/cmake/header +# fallbacks) which can be slow on Windows/MSYS2 filesystems — this keeps the +# status command visibly moving instead of appearing to hang. +printLibInlineSection(){ + local title="$1" + shift + printf '\n' + printf ' %s%s%s %s────────────────%s\n' "$C_ACCENT" "$title" "$C_RESET" "$C_MUTED" "$C_RESET" + if [[ $# -eq 0 ]]; then + printf ' %s—%s\n' "$C_MUTED" "$C_RESET" + return 0 + fi - FORMATTED_LIB_LINE="" - FORMATTED_LIB_COUNT=0 + local -a items=("$@") + local path name note entry n=0 for path in "${items[@]}"; do note="" @@ -542,43 +551,26 @@ formatLibInlineList(){ fi [[ -d "$path" ]] || continue name=$(basename "$path") + + if isInteractive; then + printf ' %s·%s %s%s%s' "$C_MUTED" "$C_RESET" "$C_FG" "$name" "$C_RESET" + fi + if readLibVersionMeta "$path"; then - if [[ -n "$note" ]]; then - entry="${name} (${LIB_META_VER} · ${note})" - else - entry="${name} (${LIB_META_VER})" - fi + entry="${name} (${LIB_META_VER}${note:+ · ${note}})" else - if [[ -n "$note" ]]; then - entry="${name} (? · ${note})" - else - entry="${name} (?)" - fi + entry="${name} (?${note:+ · ${note}})" + fi + + if isInteractive; then + printf '\r\033[K %s·%s %s\n' "$C_MUTED" "$C_RESET" "$entry" + else + printf ' · %s\n' "$entry" fi - [[ -n "$line" ]] && line+=", " - line+="$entry" n=$((n + 1)) done - FORMATTED_LIB_LINE="$line" - FORMATTED_LIB_COUNT=$n -} - -printLibInlineSection(){ - local title="$1" - shift - printf '\n' - printf ' %s%s%s %s────────────────%s\n' "$C_ACCENT" "$title" "$C_RESET" "$C_MUTED" "$C_RESET" - if [[ $# -eq 0 ]]; then - printf ' %s—%s\n' "$C_MUTED" "$C_RESET" - return 0 - fi - formatLibInlineList "$@" - if [[ "$FORMATTED_LIB_COUNT" -eq 0 ]]; then - printf ' %s—%s\n' "$C_MUTED" "$C_RESET" - return 0 - fi - printf ' %s\n' "$FORMATTED_LIB_LINE" + [[ "$n" -eq 0 ]] && printf ' %s—%s\n' "$C_MUTED" "$C_RESET" } # bash-3.2 safe: track seen keys as newline-separated string @@ -1546,9 +1538,11 @@ cmdSetup(){ elif menuCanRun && confirmYes "Install media codecs too?"; then doCodecs=1 fi fi + elif [[ "$OF_PLATFORM" == "osx" ]]; then + depsScript="${OF_CORE_SCRIPT_DIR}/osx/install_dependencies.sh" fi - if [[ "$needLibs" -eq 0 && "$needPG" -eq 0 && "$OF_PLATFORM" != "linux" ]]; then + if [[ "$needLibs" -eq 0 && "$needPG" -eq 0 && "$OF_PLATFORM" != "linux" && "$OF_PLATFORM" != "osx" ]]; then echoSuccess "already set up — libs + PG look current" echoNote "use Update to redownload from a chosen source" printf '\n' @@ -1558,6 +1552,7 @@ cmdSetup(){ taskNames+=("Detect platform") [[ "$OF_PLATFORM" == "linux" ]] && taskNames+=("Install dependencies (${OF_LINUX_DISTRO})") [[ "$doCodecs" -eq 1 ]] && taskNames+=("Install codecs (${OF_LINUX_DISTRO})") + [[ "$OF_PLATFORM" == "osx" ]] && taskNames+=("Xcode CLT / Homebrew (cmake, gum)") if [[ "$needLibs" -eq 1 ]]; then taskNames+=("Download libraries (apothecary @ latest)") else @@ -1590,6 +1585,16 @@ cmdSetup(){ fi taskN=$((taskN + 1)) fi + elif [[ "$OF_PLATFORM" == "osx" ]]; then + if [[ -f "$depsScript" ]]; then + ensureScript "$depsScript" 2>/dev/null + # best-effort: CLT/Homebrew are checked, not required to proceed — libs/PG + # come from openFrameworks' own apothecary download either way. + taskLive "$taskN" -- "$depsScript" -y + else + taskTickLine "$taskN" skip + fi + taskN=$((taskN + 1)) fi # libraries — only if missing/outdated @@ -1623,51 +1628,192 @@ cmdSetup(){ # Cleanup — projects / caches / libraries # --------------------------------------------------------------------------- -# Clean build products under a project tree (obj, bin artifacts, Xcode/VS/cmake) +# KB-on-disk for existing paths (0 for none) — one batched `du`, not per-path +pathsSizeKB(){ + local -a existing=() + local p + for p in "$@"; do + [[ -e "$p" ]] && existing+=("$p") + done + [[ ${#existing[@]} -eq 0 ]] && { printf '0'; return 0; } + du -ck "${existing[@]}" 2>/dev/null | tail -1 | awk '{print $1}' +} + +formatKB(){ + awk -v kb="${1:-0}" 'BEGIN{printf "%.1f MB", kb/1024}' +} + +# True if $1 (or anything under it) is tracked by git — used to keep the +# nuclear/platform lib-cleanup commands from deleting committed, vendored +# addon source (e.g. addons/ofxKinect/libs/libfreenect, ofxEmscripten's +# html5audio/html5video/cors) that happens to live under a libs/ or +# lib// path alongside real downloaded prebuilt binaries. +# Best-effort: if this isn't a git checkout (e.g. a release zip) or git +# isn't available, returns 1 (not tracked) so behavior is unchanged there. +pathIsGitTracked(){ + local p="$1" + command -v git >/dev/null 2>&1 || return 1 + git -C "$OF_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1 + [[ -n "$(git -C "$OF_DIR" ls-files -- "$p" 2>/dev/null | head -1)" ]] +} + +# Best-effort "send to trash/recycle bin" for a batch of paths instead of a +# permanent rm -rf — one external call for the whole batch (trashing dozens +# of build artifacts one at a time would reintroduce the per-item slowness +# cleanup was just fixed for). Falls back to permanent delete if no trash +# mechanism is available. Sets TRASH_METHOD to "trash" or "rm" so callers can +# report "moved to Trash" vs "freed" accurately — Trash/Recycle Bin doesn't +# actually reclaim disk space until it's emptied. +trashPaths(){ + local -a paths=() + local p + for p in "$@"; do + [[ -e "$p" ]] && paths+=("$p") + done + TRASH_METHOD="rm" + [[ ${#paths[@]} -eq 0 ]] && return 0 + + case "$OF_PLATFORM" in + vs|msys2) + if command -v powershell.exe >/dev/null 2>&1; then + local wp psList="" + for p in "${paths[@]}"; do + wp=$(command -v cygpath >/dev/null 2>&1 && cygpath -w "$p" 2>/dev/null || printf '%s' "$p") + wp="${wp//\'/\'\'}" + psList+="'${wp}'," + done + psList="${psList%,}" + local psCmd="Add-Type -AssemblyName Microsoft.VisualBasic; foreach (\$p in @(${psList})) { if (Test-Path -LiteralPath \$p -PathType Container) { [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(\$p, 'OnlyErrorDialogs', 'SendToRecycleBin') } elseif (Test-Path -LiteralPath \$p) { [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(\$p, 'OnlyErrorDialogs', 'SendToRecycleBin') } }" + powershell.exe -NoProfile -NonInteractive -Command "$psCmd" >/dev/null 2>&1 && TRASH_METHOD="trash" + fi + ;; + osx) + if command -v trash >/dev/null 2>&1 && trash -F "${paths[@]}" >/dev/null 2>&1; then + TRASH_METHOD="trash" + else + local asList="" esc + for p in "${paths[@]}"; do + esc="${p//\"/\\\"}" + asList+="POSIX file \"${esc}\", " + done + asList="${asList%, }" + osascript -e "tell application \"Finder\" to delete {${asList}}" >/dev/null 2>&1 && TRASH_METHOD="trash" + fi + ;; + linux) + if command -v gio >/dev/null 2>&1 && gio trash "${paths[@]}" >/dev/null 2>&1; then + TRASH_METHOD="trash" + elif command -v trash-put >/dev/null 2>&1 && trash-put "${paths[@]}" >/dev/null 2>&1; then + TRASH_METHOD="trash" + elif command -v trash >/dev/null 2>&1 && trash "${paths[@]}" >/dev/null 2>&1; then + TRASH_METHOD="trash" + fi + ;; + esac + + [[ "$TRASH_METHOD" == "trash" ]] || rm -rf "${paths[@]}" 2>/dev/null || true +} + +# Fast, stat-only check — no `find`, no `make clean` invocation. Lets callers +# skip the expensive deep clean entirely for projects with nothing built +# (this is what made "clean all examples" slow, especially over MSYS2/Windows +# filesystems where every extra process/find is costly). +projectHasBuildArtifacts(){ + local project="$1" + [[ -d "${project}/obj" ]] && return 0 + [[ -d "${project}/build" ]] && return 0 + [[ -d "${project}/.vs" ]] && return 0 + [[ -d "${project}/cmake-build-debug" ]] && return 0 + [[ -d "${project}/cmake-build-release" ]] && return 0 + [[ -d "${project}/DerivedData" ]] && return 0 + compgen -G "${project}"/*.xcodeproj/xcuserdata >/dev/null 2>&1 && return 0 + compgen -G "${project}"/*.xcodeproj/project.xcworkspace >/dev/null 2>&1 && return 0 + return 1 +} + +# Cleans one project dir. Sets CLEAN_TREE_DID_CLEAN (0/1), CLEAN_TREE_FREED_KB, +# and CLEAN_TREE_METHOD ("trash"|"rm"). Never touches /bin — that's +# where oF projects keep bin/data (assets) alongside the built binary, so it's +# left alone entirely. `make clean` (the project's own Makefile) may still +# remove its own built binary from bin/ — that's the Makefile's business, not +# ours; we don't add any rm/find of our own under bin/. cleanProjectTree(){ local project="$1" - local deep="${2:-0}" + CLEAN_TREE_DID_CLEAN=0 + CLEAN_TREE_FREED_KB=0 + CLEAN_TREE_METHOD="rm" [[ -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 \( \ + + # nothing built here — skip make/find entirely + projectHasBuildArtifacts "$project" || return 0 + + local -a targets=( + "${project}/obj" + "${project}/build" + "${project}/.vs" + "${project}/cmake-build-debug" + "${project}/cmake-build-release" + "${project}/DerivedData" + ) + + # exclude bin/ and anything already covered by $targets, so the deep finds + # below don't re-match (and double-count/double-trash) files inside them + local -a prune=(-not -path "${project}/bin/*") + local t + for t in "${targets[@]}"; do + prune+=(-not -path "${t}/*") + done + + local -a deepTargets=() + local g + while IFS= read -r -d '' g; do + deepTargets+=("$g") + done < <(find "$project" -maxdepth 3 "${prune[@]}" \( \ -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 \( \ + \) -print0 2>/dev/null) + while IFS= read -r -d '' g; do + deepTargets+=("$g") + done < <(find "$project" -maxdepth 3 "${prune[@]}" \( \ -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 + \) -type d -path '*/obj/*' -print0 2>/dev/null) + + local freedKB + freedKB=$(pathsSizeKB "${targets[@]}" "${deepTargets[@]}") + + if [[ -f "${project}/Makefile" ]] || [[ -f "${project}/makefile" ]]; then + ( cd "$project" && make clean >/dev/null 2>&1 ) || true fi + + trashPaths "${targets[@]}" "${deepTargets[@]}" + CLEAN_TREE_METHOD="$TRASH_METHOD" + + CLEAN_TREE_DID_CLEAN=1 + CLEAN_TREE_FREED_KB=$freedKB return 0 } +# Clean one project dir and print its ✓/– result line +cleanProjectsScopeOne(){ + local proj="$1" rel="$2" + cleanProjectTree "$proj" + if [[ "$CLEAN_TREE_DID_CLEAN" == "1" ]]; then + local verb="freed" + [[ "$CLEAN_TREE_METHOD" == "trash" ]] && verb="moved to Trash" + printf ' %s✓%s %s%s%s %s(%s %s)%s\n' \ + "$C_OK" "$C_RESET" "$C_FG" "$rel" "$C_RESET" "$C_MUTED" "$(formatKB "$CLEAN_TREE_FREED_KB")" "$verb" "$C_RESET" + else + printf ' %s–%s %s%s%s %s(nothing to clean)%s\n' \ + "$C_MUTED" "$C_RESET" "$C_FG" "$rel" "$C_RESET" "$C_MUTED" "$C_RESET" + fi +} + # 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 root cleaned=0 skipped=0 totalFreedKB=0 local -a roots=() case "$scope" in @@ -1683,7 +1829,7 @@ cleanProjectsScope(){ printBanner "clean" echoInfo "cleanup projects · ${scope}${path:+ · $path}" - echoKV "deep" "$([[ "$deep" == "1" ]] && echo yes || echo no)" + echoNote "bin/ (incl. bin/data) is never touched — only obj/build/.vs/etc." printf '\n' local cat ex proj rel @@ -1696,9 +1842,13 @@ cleanProjectsScope(){ if [[ -d "${cat}src" || -f "${cat}Makefile" ]]; then proj="${cat%/}" rel="${proj#"$OF_DIR"/}" - echoNote "clean ${rel}" - cleanProjectTree "$proj" "$deep" - count=$((count + 1)) + cleanProjectsScopeOne "$proj" "$rel" + if [[ "$CLEAN_TREE_DID_CLEAN" == "1" ]]; then + cleaned=$((cleaned + 1)) + totalFreedKB=$((totalFreedKB + CLEAN_TREE_FREED_KB)) + else + skipped=$((skipped + 1)) + fi continue fi for ex in "$cat"*/; do @@ -1708,20 +1858,25 @@ cleanProjectsScope(){ || compgen -G "${ex}*.vcxproj" >/dev/null 2>&1; then proj="${ex%/}" rel="${proj#"$OF_DIR"/}" - echoNote "clean ${rel}" - cleanProjectTree "$proj" "$deep" - count=$((count + 1)) + cleanProjectsScopeOne "$proj" "$rel" + if [[ "$CLEAN_TREE_DID_CLEAN" == "1" ]]; then + cleaned=$((cleaned + 1)) + totalFreedKB=$((totalFreedKB + CLEAN_TREE_FREED_KB)) + else + skipped=$((skipped + 1)) + fi fi done done done - echoSuccess "cleaned ${count} project folder(s)" + printf '\n' + echoSuccess "cleaned ${cleaned} project(s) · skipped ${skipped} (nothing to clean) · freed $(formatKB "$totalFreedKB")" } cleanDownloadCaches(){ local mode="${1:-packages}" # packages|all local dl="${OF_DIR}/libs/download" - local freed=0 + local freedKB=0 printBanner "clean" echoInfo "clean caches · ${mode}" printf '\n' @@ -1732,30 +1887,42 @@ cleanDownloadCaches(){ case "$mode" in packages) # package archives + sidecars; keep folder + local -a files=() local f n=0 while IFS= read -r f; do - [[ -f "$f" ]] || continue - rm -f "$f" - n=$((n + 1)) + [[ -f "$f" ]] && files+=("$f") 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" + freedKB=$(pathsSizeKB "${files[@]}") + for f in "${files[@]}"; do + rm -f "$f" + n=$((n + 1)) + done + echoSuccess "removed ${n} cached package file(s) under libs/download · freed $(formatKB "$freedKB")" ;; all) echoWarning "removing entire libs/download/" + freedKB=$(pathsSizeKB "$dl") rm -rf "$dl" mkdir -p "$dl" - echoSuccess "libs/download cleared" + echoSuccess "libs/download cleared · freed $(formatKB "$freedKB")" ;; *) 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" + local -a junk=() + while IFS= read -r f; do + [[ -d "$f" ]] && junk+=("$f") + done < <(find "${OF_DIR}/libs/openFrameworksCompiled" -type d \( -name 'obj' -o -name 'intermediates' \) 2>/dev/null) + if [[ ${#junk[@]} -gt 0 ]]; then + local junkKB + junkKB=$(pathsSizeKB "${junk[@]}") + rm -rf "${junk[@]}" 2>/dev/null || true + echoNote "also cleared openFrameworksCompiled obj/intermediates · freed $(formatKB "$junkKB")" + fi fi } @@ -1763,7 +1930,7 @@ cleanDownloadCaches(){ cleanLibraries(){ local mode="$1" # other-platforms|platform|all-prebuilt|list local plat="${2:-}" - local libDir d name p count=0 + local libDir d name p count=0 skipped=0 totalFreedKB=0 dKB printBanner "clean" echoInfo "libraries · ${mode}${plat:+ · $plat}" @@ -1790,9 +1957,6 @@ cleanLibraries(){ *) 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 @@ -1802,61 +1966,89 @@ cleanLibraries(){ case "$name" in pkgconfig|cmake|cmake-build*|include) continue ;; esac - echoNote "rm $d" + if pathIsGitTracked "$d"; then + skipped=$((skipped + 1)) + printf ' %s–%s %s %s(git-tracked source — skipped)%s\n' "$C_MUTED" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$C_RESET" + continue + fi + dKB=$(pathsSizeKB "$d") rm -rf "$d" + totalFreedKB=$((totalFreedKB + dKB)) count=$((count + 1)) + printf ' %s✓%s %s %s(%s freed)%s\n' "$C_OK" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$(formatKB "$dKB")" "$C_RESET" 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)" + printf '\n' + echoSuccess "removed ${count} non-host platform lib folder(s) · skipped ${skipped} (git-tracked) · freed $(formatKB "$totalFreedKB")" ;; 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" + if pathIsGitTracked "$d"; then + skipped=$((skipped + 1)) + printf ' %s–%s %s %s(git-tracked source — skipped)%s\n' "$C_MUTED" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$C_RESET" + continue + fi + dKB=$(pathsSizeKB "$d") rm -rf "$d" + totalFreedKB=$((totalFreedKB + dKB)) count=$((count + 1)) + printf ' %s✓%s %s %s(%s freed)%s\n' "$C_OK" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$(formatKB "$dKB")" "$C_RESET" done < <(find "${OF_DIR}/libs" "${OF_DIR}/addons" -type d -path "*/lib/${plat}" 2>/dev/null) - echoSuccess "removed ${count} path(s) for platform ${plat}" + printf '\n' + echoSuccess "removed ${count} path(s) for platform ${plat} · skipped ${skipped} (git-tracked) · freed $(formatKB "$totalFreedKB")" ;; 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; } + echoWarning "Also keeps anything git-tracked (vendored addon source, e.g. ofxKinect/libs/libfreenect)." 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}" + if pathIsGitTracked "$d"; then + skipped=$((skipped + 1)) + printf ' %s–%s libs/%s %s(git-tracked source — skipped)%s\n' "$C_MUTED" "$C_RESET" "$name" "$C_MUTED" "$C_RESET" + continue + fi + dKB=$(pathsSizeKB "$d") rm -rf "$d" + totalFreedKB=$((totalFreedKB + dKB)) count=$((count + 1)) + printf ' %s✓%s libs/%s %s(%s freed)%s\n' "$C_OK" "$C_RESET" "$name" "$C_MUTED" "$(formatKB "$dKB")" "$C_RESET" done # addon binary folders only for d in "${OF_DIR}/addons"/*/libs; do [[ -d "$d" ]] || continue - echoNote "rm ${d#"$OF_DIR"/}" + if pathIsGitTracked "$d"; then + skipped=$((skipped + 1)) + printf ' %s–%s %s %s(git-tracked source — skipped)%s\n' "$C_MUTED" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$C_RESET" + continue + fi + dKB=$(pathsSizeKB "$d") rm -rf "$d" + totalFreedKB=$((totalFreedKB + dKB)) count=$((count + 1)) + printf ' %s✓%s %s %s(%s freed)%s\n' "$C_OK" "$C_RESET" "${d#"$OF_DIR"/}" "$C_MUTED" "$(formatKB "$dKB")" "$C_RESET" done rm -f "${OF_DIR}/libs/.of-cli-state" 2>/dev/null || true - echoSuccess "removed ${count} prebuilt tree(s) — run Update libs to restore" + printf '\n' + echoSuccess "removed ${count} prebuilt tree(s) · skipped ${skipped} (git-tracked) · freed $(formatKB "$totalFreedKB") — run Update libs to restore" ;; *) echoError "mode: list|other-platforms|platform|all-prebuilt"; return 1 ;; esac } menuCleanup(){ - local choice scope deep=0 plat + local choice scope plat printBanner "clean" echoInfo "cleanup projects · caches · libraries" printf '\n' while true; do menuPick "Cleanup" \ - "Projects — obj/bin/build artifacts|projects" \ + "Projects — obj/build/.vs artifacts (bin/ untouched)|projects" \ "Caches — libs/download packages|caches" \ "Libraries — remove / minimise prebuilts|libs" \ "Back|back" \ @@ -1872,18 +2064,11 @@ menuCleanup(){ "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" + cleanProjectsScope path "$UI_INPUT_RESULT" else - confirmYes "Clean ${scope} project build artifacts?" || continue - cleanProjectsScope "$scope" "" "$deep" + cleanProjectsScope "$scope" fi menuPause ;; @@ -1893,16 +2078,15 @@ menuCleanup(){ "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 ALL prebuilt libs (nuclear)|all" \ "Remove other platforms (keep this host) — Recommended|other" \ "Remove one platform…|one" \ - "Remove ALL prebuilt libs (nuclear)|all" \ + "List installed lib/ folders|list" \ "Back|back" || continue case "$UI_MENU_RESULT" in list) cleanLibraries list; menuPause ;; @@ -1929,13 +2113,13 @@ menuCleanup(){ } cmdMenu(){ - local choice setupLabel="Setup" + local choice setupLabel="Setup — libs + Project Generator (apothecary @ latest)" if ! menuCanRun; then echoWarning "no TTY — showing status" cmdStatus return 0 fi - [[ "$OF_PLATFORM" == "linux" ]] && setupLabel="Setup (distro deps + libs + PG)" + [[ "$OF_PLATFORM" == "linux" ]] && setupLabel="Setup — distro deps + libs + Project Generator" while true; do printf '\n' @@ -2030,7 +2214,7 @@ cmdBuild(){ [[ -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 + if [[ "${1:-}" =~ ^(make|msbuild|xcode|xcodebuild|emscripten|em|wasm|cmake|generate|pg|host)$ ]]; then system="$1"; shift || true fi [[ -n "${1:-}" ]] && config="$1" @@ -2040,7 +2224,7 @@ cmdBuild(){ 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 + if [[ "${1:-}" =~ ^(make|msbuild|xcode|xcodebuild|emscripten|em|wasm|cmake|generate|pg|host)$ ]]; then system="$1"; shift || true fi [[ -n "${1:-}" ]] && config="$1" @@ -2122,8 +2306,8 @@ runCommand(){ ""|menu) menuCleanup ;; projects|project) case "${subcmd2:-all}" in - apps|examples|all) cleanProjectsScope "${subcmd2:-all}" "" 0 ;; - *) cleanProjectsScope path "${subcmd2}" 0 ;; + apps|examples|all) cleanProjectsScope "${subcmd2:-all}" ;; + *) cleanProjectsScope path "${subcmd2}" ;; esac ;; caches|cache) diff --git a/scripts/of_build.sh b/scripts/of_build.sh index 837a231694c..9eeb0205973 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -309,7 +309,17 @@ menuPickBuildSystem(){ local hostPg hostPg=$(ofHostPgPlatform) - opts+=("make (${hostPg} / host)|make") + local hasVcxproj=0 + compgen -G "${project}/*.vcxproj" >/dev/null 2>&1 && hasVcxproj=1 + + if [[ "$OF_PLATFORM" == "vs" || "$hasVcxproj" -eq 1 ]]; then + opts+=("msbuild (Visual Studio)|msbuild") + fi + # "make" needs a Makefile-based host toolchain; on native VS (MSVC) that's not + # the normal path, but keep it available for msys2/mingw-style builds. + if [[ "$OF_PLATFORM" != "vs" ]]; then + opts+=("make (${hostPg} / host)|make") + fi if [[ "$(uname -s)" == "Darwin" ]] && compgen -G "${project}/*.xcodeproj" >/dev/null 2>&1; then opts+=("xcodebuild|xcode") fi @@ -321,6 +331,134 @@ menuPickBuildSystem(){ menuPick "Build system · $(basename "$project")" "${opts[@]}" || return 1 } +# --------------------------------------------------------------------------- +# Visual Studio / MSBuild helpers +# --------------------------------------------------------------------------- +# Common vswhere.exe locations under MSYS2/Git-Bash (C:\ → /c/) +vsWhereBinary(){ + local c + for c in \ + "/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" \ + "/c/Program Files/Microsoft Visual Studio/Installer/vswhere.exe" + do + [[ -f "$c" ]] && { printf '%s' "$c"; return 0; } + done + return 1 +} + +# Locate MSBuild.exe: PATH first (Developer Command Prompt), else vswhere +findMsbuildBinary(){ + if command -v msbuild.exe >/dev/null 2>&1; then + command -v msbuild.exe + return 0 + fi + if command -v MSBuild.exe >/dev/null 2>&1; then + command -v MSBuild.exe + return 0 + fi + local vswhere installPath candidate + vswhere=$(vsWhereBinary) || return 1 + installPath=$("$vswhere" -latest -prerelease -products '*' \ + -requires Microsoft.Component.MSBuild -property installationPath 2>/dev/null | tr -d '\r') + [[ -n "$installPath" ]] || return 1 + command -v cygpath >/dev/null 2>&1 && installPath=$(cygpath -u "$installPath") + candidate="${installPath}/MSBuild/Current/Bin/MSBuild.exe" + [[ -f "$candidate" ]] && { printf '%s' "$candidate"; return 0; } + return 1 +} + +# Host MSBuild Platform token (x64 | ARM64) from detected VS arch (64 | arm64) +vsPlatformFromArch(){ + local arch + arch=$(command -v detectHostVsArch >/dev/null 2>&1 && detectHostVsArch || printf '64') + case "$arch" in + arm64) printf 'ARM64' ;; + *) printf 'x64' ;; + esac +} + +# PlatformToolset default — v145 (VS2026) unless the newest installed VS is clearly 2022 (17.x) +vsToolsetDefault(){ + local vswhere major + vswhere=$(vsWhereBinary) || { printf 'v145'; return 0; } + major=$("$vswhere" -latest -prerelease -products '*' -property installationVersion 2>/dev/null | tr -d '\r' | cut -d. -f1) + case "$major" in + 17) printf 'v143' ;; + *) printf 'v145' ;; + esac +} + +menuPickVsToolset(){ + local def + def=$(vsToolsetDefault) + menuPick "PlatformToolset (detected: ${def})" \ + "v145 — Visual Studio 2026|v145" \ + "v143 — Visual Studio 2022|v143" \ + "Custom…|custom" \ + || return 1 + if [[ "$UI_MENU_RESULT" == "custom" ]]; then + menuInput "PlatformToolset (e.g. v143, v145, ClangCL)" "$def" || return 1 + UI_MENU_RESULT="$UI_INPUT_RESULT" + fi + return 0 +} + +menuPickVsPlatform(){ + local def + def=$(vsPlatformFromArch) + menuPick "Platform (host: ${def})" \ + "x64|x64" \ + "ARM64|ARM64" \ + "ARM64EC|ARM64EC" \ + || return 1 + return 0 +} + +# PG "-t" template: vs2026 (default) vs vs2022 (empty → PG's legacy default) +menuPickPgTemplate(){ + menuPick "Visual Studio project template" \ + "Visual Studio 2026 (default)|vs2026" \ + "Visual Studio 2022|" \ + || return 1 + return 0 +} + +findVcxproj(){ + compgen -G "${1}/*.vcxproj" 2>/dev/null | head -1 +} + +runMsbuildProject(){ + local project="$1" + local config="${2:-Release}" + local toolset="${3:-}" + local platform="${4:-}" + local vcxproj msbuild + + vcxproj=$(findVcxproj "$project") + [[ -n "$vcxproj" ]] || { + echoError "no .vcxproj in $(basename "$project")" + echoNote "generate one first: of build generate \"$project\"" + return 1 + } + + msbuild=$(findMsbuildBinary) || { + echoError "msbuild.exe not found" + echoNote "install the 'Desktop development with C++' workload, or run from a Developer Command Prompt with VS on PATH" + return 1 + } + + [[ -n "$toolset" ]] || toolset="${OF_VS_TOOLSET:-$(vsToolsetDefault)}" + [[ -n "$platform" ]] || platform="${OF_VS_PLATFORM:-$(vsPlatformFromArch)}" + + echoInfo "msbuild ${config} · $(basename "$vcxproj") · ${platform} · toolset ${toolset}" + echoKV "msbuild" "$msbuild" + "$msbuild" "$vcxproj" \ + "/p:Configuration=${config}" \ + "/p:Platform=${platform}" \ + "/p:PlatformToolset=${toolset}" \ + /nologo /m +} + # --------------------------------------------------------------------------- # Project file helpers # --------------------------------------------------------------------------- @@ -365,6 +503,7 @@ runProjectGenerator(){ local project="$1" shift local platforms="${1:-}" + local pgTemplate="${2:-}" local bin bin=$(findPGBinary) || { echoError "Project Generator not found — run: of update pg" @@ -374,11 +513,71 @@ runProjectGenerator(){ if [[ -n "$platforms" ]]; then cmd+=( -p"$platforms" ) fi + if [[ -n "$pgTemplate" ]]; then + cmd+=( -t"$pgTemplate" ) + fi cmd+=( "$project" ) echoNote "${cmd[*]}" PG_OF_PATH="$OF_DIR" "${cmd[@]}" } +ideNameForPlatform(){ + case "$OF_PLATFORM" in + vs) printf 'Visual Studio' ;; + osx|macos) printf 'Xcode' ;; + linux) printf 'Qt Creator / Code::Blocks' ;; + *) printf 'IDE' ;; + esac +} + +# Best-effort native-IDE launch after generating project files +openGeneratedProject(){ + local project="$1" + local proj + case "$OF_PLATFORM" in + vs) + proj=$(compgen -G "${project}/*.sln" 2>/dev/null | head -1) + [[ -z "$proj" ]] && proj=$(findVcxproj "$project") + [[ -n "$proj" ]] || { echoWarning "no .sln/.vcxproj to open"; return 1; } + echoInfo "opening $(basename "$proj") in Visual Studio" + if command -v cygpath >/dev/null 2>&1; then + cmd.exe /c start "" "$(cygpath -w "$proj")" >/dev/null 2>&1 & + else + start "" "$proj" >/dev/null 2>&1 & + fi + disown 2>/dev/null || true + ;; + osx|macos) + proj=$(compgen -G "${project}/*.xcodeproj" 2>/dev/null | head -1) + [[ -n "$proj" ]] || { echoWarning "no .xcodeproj to open"; return 1; } + echoInfo "opening $(basename "$proj") in Xcode" + open "$proj" + ;; + linux) + proj=$(compgen -G "${project}/*.qbs" 2>/dev/null | head -1) + if [[ -n "$proj" ]] && command -v qtcreator >/dev/null 2>&1; then + echoInfo "opening $(basename "$proj") in Qt Creator" + qtcreator "$proj" >/dev/null 2>&1 & + disown 2>/dev/null || true + return 0 + fi + proj=$(compgen -G "${project}/*.workspace" 2>/dev/null | head -1) + if [[ -n "$proj" ]] && command -v codeblocks >/dev/null 2>&1; then + echoInfo "opening $(basename "$proj") in Code::Blocks" + codeblocks "$proj" >/dev/null 2>&1 & + disown 2>/dev/null || true + return 0 + fi + echoNote "no known IDE found on PATH — project files are in $(basename "$project")" + return 1 + ;; + *) + echoNote "no IDE launch wired up for platform '${OF_PLATFORM}'" + return 0 + ;; + esac +} + # --------------------------------------------------------------------------- # Build runners # --------------------------------------------------------------------------- @@ -652,6 +851,9 @@ cmdBuildProject(){ local system="${2:-make}" local config="${3:-Release}" local openAfter="${4:-0}" + # system-specific extras: msbuild → toolset/platform; generate → PG template + local extra1="${5:-}" + local extra2="${6:-}" project=$(resolveProjectPath "$project") || { echoError "project not found: $1" @@ -666,6 +868,10 @@ cmdBuildProject(){ printf '\n' case "$system" in + msbuild|vs|vcxproj) + runMsbuildProject "$project" "$config" "$extra1" "$extra2" || return $? + [[ "$openAfter" == "1" ]] && runNativeApp "$project" "$config" + ;; make|host) runMakeInProject "$project" "$config" || return $? [[ "$openAfter" == "1" ]] && runNativeApp "$project" "$config" @@ -690,14 +896,14 @@ cmdBuildProject(){ generate|pg) local plats plats=$(ofHostPgPlatform) - runProjectGenerator "$project" "$plats" || return $? + runProjectGenerator "$project" "$plats" "$extra1" || return $? ;; clean) runCleanProject "$project" || return $? ;; *) echoError "unknown build system: $system" - echoNote "valid: make xcode emscripten cmake generate clean" + echoNote "valid: make msbuild xcode emscripten cmake generate clean" return 1 ;; esac @@ -715,16 +921,24 @@ cmdBuildCore(){ cmdBuildGenerate(){ local project="$1" local platforms="${2:-}" + local pgTemplate="${3:-}" + local openAfter="${4:-0}" project=$(resolveProjectPath "$project") || { echoError "path not found: $1" return 1 } [[ -n "$platforms" ]] || platforms=$(ofHostPgPlatform) + # default the VS project template to 2026 when generating for vs and none was given + if [[ -z "$pgTemplate" && "$platforms" == *vs* && "$OF_PLATFORM" == "vs" ]]; then + pgTemplate="${OF_PG_TEMPLATE:-vs2026}" + fi printBanner "generate" echoKV "project" "$project" echoKV "platforms" "$platforms" - runProjectGenerator "$project" "$platforms" || return $? + [[ -n "$pgTemplate" ]] && echoKV "vs template" "$pgTemplate" + runProjectGenerator "$project" "$platforms" "$pgTemplate" || return $? echoSuccess "project files updated" + [[ "$openAfter" == "1" ]] && openGeneratedProject "$project" } # --------------------------------------------------------------------------- @@ -741,7 +955,7 @@ menuBuildProjectActions(){ if [[ "$system" == "generate" ]]; then local -a popts=() - local p plats="" + local p plats="" pgTemplate="" openAfterGen=0 for p in osx macos emscripten ios linux64 linux vs msys2 android tvos; do popts+=("${p}|${p}") done @@ -752,7 +966,24 @@ menuBuildProjectActions(){ fi fi [[ -z "$plats" ]] && plats=$(ofHostPgPlatform) - cmdBuildGenerate "$project" "$plats" + if [[ "$OF_PLATFORM" == "vs" && "$plats" == *vs* ]] && menuCanRun; then + menuPickPgTemplate && pgTemplate="$UI_MENU_RESULT" + fi + if menuCanRun && confirmYes "Open generated project in $(ideNameForPlatform) when done?"; then + openAfterGen=1 + fi + cmdBuildGenerate "$project" "$plats" "$pgTemplate" "$openAfterGen" + return $? + fi + + if [[ "$system" == "msbuild" ]]; then + menuPickConfig || return 0 + config="$UI_MENU_RESULT" + menuPickVsToolset || return 0 + local toolset="$UI_MENU_RESULT" + menuPickVsPlatform || return 0 + local platform="$UI_MENU_RESULT" + cmdBuildProject "$project" msbuild "$config" 0 "$toolset" "$platform" return $? fi @@ -953,12 +1184,18 @@ menuBuild(){ ;; *) continue ;; esac - local plats + local plats pgTemplate="" openAfterGen=0 plats=$(ofHostPgPlatform) if confirmYes "Also add emscripten platform files?"; then plats="${plats},emscripten" fi - cmdBuildGenerate "$project" "$plats" + if [[ "$OF_PLATFORM" == "vs" && "$plats" == *vs* ]]; then + menuPickPgTemplate && pgTemplate="$UI_MENU_RESULT" + fi + if confirmYes "Open generated project in $(ideNameForPlatform) when done?"; then + openAfterGen=1 + fi + cmdBuildGenerate "$project" "$plats" "$pgTemplate" "$openAfterGen" menuPause ;; cmake) @@ -1012,12 +1249,15 @@ printHelpBuild(){ ${prog} build open-em Open emscripten index.html (Chrome/emrun) Systems (project) - make (default) xcode emscripten cmake generate + make (default) msbuild xcode emscripten cmake generate Env OF_JOBS=8 Parallel jobs OF_CMAKE_ARGS="..." Extra cmake -S/-B flags EMCC_DEBUG=1 Emscripten debug logging + OF_VS_TOOLSET=v145 MSBuild PlatformToolset (v145=VS2026, v143=VS2022) + OF_VS_PLATFORM=x64 MSBuild Platform (x64 | ARM64 | ARM64EC) + OF_PG_TEMPLATE=vs2026 Project Generator "-t" template when generating for vs Examples ${prog} build core Debug @@ -1025,6 +1265,7 @@ printHelpBuild(){ ${prog} build project examples/graphics/graphicsExample xcode Debug ${prog} build emscripten examples/graphics/graphicsExample Debug ${prog} build generate apps/myApps/mySketch osx,emscripten + OF_VS_TOOLSET=v145 OF_VS_PLATFORM=x64 ${prog} build project apps/myApps/mySketch msbuild Release EOF } diff --git a/scripts/osx/install_dependencies.sh b/scripts/osx/install_dependencies.sh new file mode 100755 index 00000000000..068e8b0c8ea --- /dev/null +++ b/scripts/osx/install_dependencies.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# install_dependencies.sh — macOS setup helper for openFrameworks +# +# macOS has no long native dependency list the way Linux distros do — the +# real build requirement is Xcode's Command Line Tools, and openFrameworks +# ships its own prebuilt libs via apothecary. This checks for the CLT, and — +# only if Homebrew is already installed — offers to install the small set of +# optional CLI tools this tooling can use (cmake, gum). It never installs +# Homebrew itself; if it's missing, those installs are just skipped. +# +# Usage: install_dependencies.sh [-y|--noconfirm] + +FORCE_YES=0 +for arg in "$@"; do + case "$arg" in + -y|--noconfirm) FORCE_YES=1 ;; + esac +done + +BREW_PACKAGES=(cmake gum) + +echo "openFrameworks · macOS dependency check" +echo + +# --- Xcode Command Line Tools ------------------------------------------ +if xcode-select -p >/dev/null 2>&1; then + echo "OK Xcode Command Line Tools: $(xcode-select -p)" +else + echo "!! Xcode Command Line Tools not found — required to build openFrameworks." + echo " Launching the installer (a system dialog will appear)…" + xcode-select --install + echo " Re-run setup once that install finishes." +fi +echo + +# --- Homebrew (optional: cmake, gum) ------------------------------------ +if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew not found on PATH — skipping optional package installs (${BREW_PACKAGES[*]})." + echo "Not installing Homebrew automatically. Get it from https://brew.sh if you want these, then re-run setup." + exit 0 +fi +echo "OK Homebrew: $(brew --version | head -1)" + +MISSING=() +for pkg in "${BREW_PACKAGES[@]}"; do + if command -v "$pkg" >/dev/null 2>&1 || brew list --formula "$pkg" >/dev/null 2>&1; then + echo "OK ${pkg} already available" + else + MISSING+=("$pkg") + fi +done + +if [[ ${#MISSING[@]} -eq 0 ]]; then + echo "All optional packages already installed." + exit 0 +fi + +echo +echo "Installing via Homebrew: ${MISSING[*]}" +if [[ "$FORCE_YES" -ne 1 ]]; then + read -r -p "Continue? [Y/n]: " CONFIRM + [[ -z "$CONFIRM" || "$CONFIRM" =~ ^[Yy]$ ]] || { echo "Skipped."; exit 0; } +fi + +brew install "${MISSING[@]}" From 76f7f9dbfe6dba0e30b1ff1b4262810f80abf964 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 01:38:29 +1000 Subject: [PATCH 06/13] package gum --- .../linux/archlinux/install_dependencies.sh | 2 +- scripts/linux/debian/install_dependencies.sh | 9 +++ scripts/linux/fedora/install_dependencies.sh | 10 +++ scripts/linux/ubuntu/install_dependencies.sh | 12 +++- scripts/vs/install_dependencies.sh | 64 +++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100755 scripts/vs/install_dependencies.sh diff --git a/scripts/linux/archlinux/install_dependencies.sh b/scripts/linux/archlinux/install_dependencies.sh index e8bb39741ff..ca2fa490f20 100755 --- a/scripts/linux/archlinux/install_dependencies.sh +++ b/scripts/linux/archlinux/install_dependencies.sh @@ -11,7 +11,7 @@ fi ROOT=$(cd $(dirname $0); pwd -P) -pacman -S --needed make pkgconf gcc openal glm glew freeglut gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav opencv libxcursor assimp glfw-x11 uriparser curl pugixml rtaudio poco brotli +pacman -S --needed make pkgconf gcc openal glm glew freeglut gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav opencv libxcursor assimp glfw-x11 uriparser curl pugixml rtaudio poco brotli gum exit_code=$? if [ $exit_code != 0 ]; then diff --git a/scripts/linux/debian/install_dependencies.sh b/scripts/linux/debian/install_dependencies.sh index 1c4b31f3aaa..608f5c6ac55 100755 --- a/scripts/linux/debian/install_dependencies.sh +++ b/scripts/linux/debian/install_dependencies.sh @@ -59,6 +59,15 @@ if [ $exit_code != 0 ]; then exit $exit_code fi +# Optional: gum (nicer of.sh interactive menu) — only if already in apt's repos. +# Not in default Debian repos as of writing; we don't add third-party repos +# automatically. If you want it, see https://github.com/charmbracelet/gum#installation +if apt-cache show gum &>/dev/null; then + echo "Package gum is available. Installing..." + apt-get install -y gum +else + echo "gum not found in configured apt repos - skipping (optional, used for of.sh's nicer interactive menu)" +fi echo "installing gstreamer" apt-get install libgstreamer${GSTREAMER_VERSION}-dev libgstreamer-plugins-base${GSTREAMER_VERSION}-dev ${GSTREAMER_FFMPEG} gstreamer${GSTREAMER_VERSION}-pulseaudio gstreamer${GSTREAMER_VERSION}-x gstreamer${GSTREAMER_VERSION}-plugins-bad gstreamer${GSTREAMER_VERSION}-alsa gstreamer${GSTREAMER_VERSION}-plugins-base gstreamer${GSTREAMER_VERSION}-plugins-good diff --git a/scripts/linux/fedora/install_dependencies.sh b/scripts/linux/fedora/install_dependencies.sh index 800c9f08c4d..1bfe20a1cb9 100755 --- a/scripts/linux/fedora/install_dependencies.sh +++ b/scripts/linux/fedora/install_dependencies.sh @@ -36,6 +36,16 @@ if [ $exit_code != 0 ]; then exit $exit_code fi +# Optional: gum (nicer of.sh interactive menu) — only if already in dnf's repos. +# Not in default Fedora repos as of writing; we don't add third-party repos +# automatically. If you want it, see https://github.com/charmbracelet/gum#installation +if dnf list --available gum &>/dev/null; then + echo "Package gum is available. Installing..." + dnf install -y gum +else + echo "gum not found in configured dnf repos - skipping (optional, used for of.sh's nicer interactive menu)" +fi + # Update addon_config.mk files to use OpenCV 3 or 4 depending on what's installed addons_dir="$(readlink -f "$ROOT/../../../addons")" $(pkg-config opencv4 --exists) diff --git a/scripts/linux/ubuntu/install_dependencies.sh b/scripts/linux/ubuntu/install_dependencies.sh index 5ba00708852..cf7a1e28754 100755 --- a/scripts/linux/ubuntu/install_dependencies.sh +++ b/scripts/linux/ubuntu/install_dependencies.sh @@ -232,12 +232,22 @@ fi apt-get -y -qq install ${PACKAGES} installPackages ${PACKAGES} -# Install libgconf-2-4 only if its availble +# Install libgconf-2-4 only if its availble if apt-cache show libgconf-2-4 &>/dev/null; then echo "Package libgconf-2-4 is available. Installing..." installPackages libgconf-2-4 fi +# Optional: gum (nicer of.sh interactive menu) — only if already in apt's repos. +# Not in default Ubuntu/Debian repos as of writing; we don't add third-party +# repos automatically. If you want it, see https://github.com/charmbracelet/gum#installation +if apt-cache show gum &>/dev/null; then + echo "Package gum is available. Installing..." + installPackages gum +else + echo "gum not found in configured apt repos - skipping (optional, used for of.sh's nicer interactive menu)" +fi + if [[ $MAJOR_VERSION -lt 18 ]]; then cp $ROOT/../extra/poco_config.mk $ROOT/../../../addons/ofxPoco/addon_config.mk fi diff --git a/scripts/vs/install_dependencies.sh b/scripts/vs/install_dependencies.sh new file mode 100755 index 00000000000..f632e65735b --- /dev/null +++ b/scripts/vs/install_dependencies.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# install_dependencies.sh — Windows setup helper for openFrameworks (MSYS2/Git-Bash) +# +# Windows has no single native dependency list the way Linux distros do — the +# real build requirement is Visual Studio (MSVC + Windows SDK), which this +# can't silently install. This only offers to install the small optional CLI +# tool this tooling can use (gum, for the nicer of.sh interactive menu) via a +# package manager already on the machine (winget, then scoop). It never +# installs a package manager itself; if neither is found, the install is +# just skipped. +# +# Usage: install_dependencies.sh [-y|--noconfirm] + +FORCE_YES=0 +for arg in "$@"; do + case "$arg" in + -y|--noconfirm) FORCE_YES=1 ;; + esac +done + +echo "openFrameworks · Windows dependency check" +echo + +installViaWinget(){ + command -v winget.exe >/dev/null 2>&1 || return 1 + echo "Trying: winget install gum" + winget.exe install --id Charmbracelet.gum -e --source winget \ + --accept-package-agreements --accept-source-agreements >/dev/null 2>&1 +} + +installViaScoop(){ + local scoopBin="" + command -v scoop >/dev/null 2>&1 && scoopBin="scoop" + [[ -z "$scoopBin" ]] && command -v scoop.exe >/dev/null 2>&1 && scoopBin="scoop.exe" + [[ -n "$scoopBin" ]] || return 1 + echo "Trying: scoop install gum (extras bucket)" + "$scoopBin" bucket add extras >/dev/null 2>&1 + "$scoopBin" install gum >/dev/null 2>&1 +} + +if command -v gum >/dev/null 2>&1 || command -v gum.exe >/dev/null 2>&1; then + echo "OK gum already available" + exit 0 +fi + +if [[ "$FORCE_YES" -ne 1 ]]; then + read -r -p "Install gum via winget/scoop if available? [Y/n]: " CONFIRM + [[ -z "$CONFIRM" || "$CONFIRM" =~ ^[Yy]$ ]] || { echo "Skipped."; exit 0; } +fi + +if installViaWinget; then + echo "OK gum installed via winget" + exit 0 +fi + +if installViaScoop; then + echo "OK gum installed via scoop" + exit 0 +fi + +echo "gum not installed — no winget/scoop package succeeded (or neither is available)." +echo "Not installing a package manager automatically." +echo "Install winget (usually preinstalled as 'App Installer') or scoop (https://scoop.sh) yourself, then re-run setup." +exit 0 From 37c7f1ad20eaf08c4a30117fe148afbe5c381d8d Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 02:23:10 +1000 Subject: [PATCH 07/13] Windows fixes for msbuild --- scripts/of_build.sh | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/scripts/of_build.sh b/scripts/of_build.sh index 9eeb0205973..4ddb920953c 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -452,11 +452,15 @@ runMsbuildProject(){ echoInfo "msbuild ${config} · $(basename "$vcxproj") · ${platform} · toolset ${toolset}" echoKV "msbuild" "$msbuild" + # dash-style switches, not /slash-style: under MSYS2/Git-Bash, a leading "/" + # looks like a POSIX absolute path and gets silently rewritten (e.g. "/m" + # becomes "M:/", "/nologo" becomes "C:/.../nologo"), which breaks msbuild's + # argument parsing entirely. MSBuild accepts "-switch" identically. "$msbuild" "$vcxproj" \ - "/p:Configuration=${config}" \ - "/p:Platform=${platform}" \ - "/p:PlatformToolset=${toolset}" \ - /nologo /m + "-p:Configuration=${config}" \ + "-p:Platform=${platform}" \ + "-p:PlatformToolset=${toolset}" \ + -nologo -m } # --------------------------------------------------------------------------- @@ -540,11 +544,14 @@ openGeneratedProject(){ [[ -z "$proj" ]] && proj=$(findVcxproj "$project") [[ -n "$proj" ]] || { echoWarning "no .sln/.vcxproj to open"; return 1; } echoInfo "opening $(basename "$proj") in Visual Studio" - if command -v cygpath >/dev/null 2>&1; then - cmd.exe /c start "" "$(cygpath -w "$proj")" >/dev/null 2>&1 & - else - start "" "$proj" >/dev/null 2>&1 & - fi + # explorer.exe, not `cmd.exe /c start`: under MSYS2/Git-Bash a leading + # "/" (like /c) looks like a POSIX path and gets silently rewritten to + # a drive letter (e.g. "/c" -> "C:/"), so cmd.exe never sees a valid + # /c switch. explorer.exe takes a plain path — no flag to mangle — + # and Windows opens .sln with its registered handler (Visual Studio). + local winProj="$proj" + command -v cygpath >/dev/null 2>&1 && winProj=$(cygpath -w "$proj") + explorer.exe "$winProj" >/dev/null 2>&1 & disown 2>/dev/null || true ;; osx|macos) From 4d32010d8fadcacc0bd9c52acde465d46b98dae8 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 02:47:29 +1000 Subject: [PATCH 08/13] Platform order --- scripts/of.sh | 16 +++++++++++++++- scripts/of_build.sh | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/scripts/of.sh b/scripts/of.sh index c7f0bff57f7..f6f2e17c7ce 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -1540,9 +1540,12 @@ cmdSetup(){ fi elif [[ "$OF_PLATFORM" == "osx" ]]; then depsScript="${OF_CORE_SCRIPT_DIR}/osx/install_dependencies.sh" + elif [[ "$OF_PLATFORM" == "vs" || "$OF_PLATFORM" == "msys2" ]]; then + depsScript="${OF_CORE_SCRIPT_DIR}/vs/install_dependencies.sh" fi - if [[ "$needLibs" -eq 0 && "$needPG" -eq 0 && "$OF_PLATFORM" != "linux" && "$OF_PLATFORM" != "osx" ]]; then + if [[ "$needLibs" -eq 0 && "$needPG" -eq 0 && "$OF_PLATFORM" != "linux" && "$OF_PLATFORM" != "osx" \ + && "$OF_PLATFORM" != "vs" && "$OF_PLATFORM" != "msys2" ]]; then echoSuccess "already set up — libs + PG look current" echoNote "use Update to redownload from a chosen source" printf '\n' @@ -1553,6 +1556,7 @@ cmdSetup(){ [[ "$OF_PLATFORM" == "linux" ]] && taskNames+=("Install dependencies (${OF_LINUX_DISTRO})") [[ "$doCodecs" -eq 1 ]] && taskNames+=("Install codecs (${OF_LINUX_DISTRO})") [[ "$OF_PLATFORM" == "osx" ]] && taskNames+=("Xcode CLT / Homebrew (cmake, gum)") + [[ "$OF_PLATFORM" == "vs" || "$OF_PLATFORM" == "msys2" ]] && taskNames+=("gum via winget/scoop (optional)") if [[ "$needLibs" -eq 1 ]]; then taskNames+=("Download libraries (apothecary @ latest)") else @@ -1595,6 +1599,16 @@ cmdSetup(){ taskTickLine "$taskN" skip fi taskN=$((taskN + 1)) + elif [[ "$OF_PLATFORM" == "vs" || "$OF_PLATFORM" == "msys2" ]]; then + if [[ -f "$depsScript" ]]; then + ensureScript "$depsScript" 2>/dev/null + # best-effort: gum is a nicety, not required — libs/PG come from + # openFrameworks' own apothecary download either way. + taskLive "$taskN" -- "$depsScript" -y + else + taskTickLine "$taskN" skip + fi + taskN=$((taskN + 1)) fi # libraries — only if missing/outdated diff --git a/scripts/of_build.sh b/scripts/of_build.sh index 4ddb920953c..6fdd7197a83 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -38,6 +38,24 @@ ofHostPgPlatform(){ esac } +# PG platform tokens, host-first: this host's own platform, then emscripten, +# then android (the next-most-common cross-compile targets), then the rest. +pgPlatformOrder(){ + local host + host=$(ofHostPgPlatform) + local -a full=(osx macos emscripten ios linux64 linux vs msys2 android tvos) + local -a ordered=() + local seen=" " + local p + for p in "$host" emscripten android "${full[@]}"; do + [[ " ${full[*]} " == *" ${p} "* ]] || continue + [[ "$seen" == *" ${p} "* ]] && continue + ordered+=("$p") + seen+="${p} " + done + printf '%s\n' "${ordered[@]}" +} + # --------------------------------------------------------------------------- # Tool discovery # --------------------------------------------------------------------------- @@ -963,9 +981,9 @@ menuBuildProjectActions(){ if [[ "$system" == "generate" ]]; then local -a popts=() local p plats="" pgTemplate="" openAfterGen=0 - for p in osx macos emscripten ios linux64 linux vs msys2 android tvos; do + while IFS= read -r p; do popts+=("${p}|${p}") - done + done < <(pgPlatformOrder) if menuCanRun; then if menuPickMulti "PG platforms (space-separated multi-select)" "${popts[@]}"; then # ids are space-separated → comma list for projectGenerator From 0a31dd9de00cc81378ae6e12251da30bb8776e1e Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 02:47:49 +1000 Subject: [PATCH 09/13] of Workflow VS2022 and VS2026 to one action --- .github/workflows/of.yml | 63 ++++++++++++---------------------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/.github/workflows/of.yml b/.github/workflows/of.yml index fe67c537779..08f95b58b08 100644 --- a/.github/workflows/of.yml +++ b/.github/workflows/of.yml @@ -118,13 +118,22 @@ jobs: # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - build-vs2022: - runs-on: windows-2022 + build-vs: + runs-on: ${{ matrix.runner }} strategy: + fail-fast: false matrix: platform: [x64, ARM64, ARM64EC] - env: - BITS: ${{ matrix.bits }} + vsYear: [2022, 2026] + include: + - vsYear: 2022 + runner: windows-2022 + toolset: v143 + installFlag: "" + - vsYear: 2026 + runner: windows-2025-vs2026 + toolset: v145 + installFlag: "--vs2026" steps: - name: Clone repository uses: actions/checkout@v6 @@ -149,56 +158,20 @@ jobs: # fi - name: Install dependencies shell: msys2 {0} - run: ./scripts/ci/vs/install.sh + run: ./scripts/ci/vs/install.sh ${{ matrix.installFlag }} -a ${{ matrix.platform }} - name: Setup MSBuild uses: microsoft/setup-msbuild@v3 - name: Build DEBUG and RELEASE emptyExample working-directory: ${{env.GITHUB_WORKSPACE}} run: | - msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:configuration=debug /p:platform=${{ matrix.platform }} /p:PlatformToolset=v143 - msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:configuration=release /p:platform=${{ matrix.platform }} /p:PlatformToolset=v143 - - name: Build DEBUG and RELEASE allAddonsExample - working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:configuration=debug /p:platform=${{ matrix.platform }} /p:PlatformToolset=v143 - msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:configuration=release /p:platform=${{ matrix.platform }} /p:PlatformToolset=v143 - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - build-vs2026: - runs-on: windows-2025-vs2026 - strategy: - matrix: - platform: [x64, ARM64, ARM64EC] - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - uses: msys2/setup-msys2@v2 - with: - update: true - install: >- - git - unzip - - - name: Install dependencies (VS2026 libs — only for this arch) - shell: msys2 {0} - run: ./scripts/ci/vs/install.sh --vs2026 -a ${{ matrix.platform }} - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v3 - - - name: Build DEBUG and RELEASE emptyExample - working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:Configuration=Debug /p:Platform=${{ matrix.platform }} /p:PlatformToolset=v145 - msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:Configuration=Release /p:Platform=${{ matrix.platform }} /p:PlatformToolset=v145 - + msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:Configuration=Debug /p:Platform=${{ matrix.platform }} /p:PlatformToolset=${{ matrix.toolset }} + msbuild examples/templates/emptyExample/emptyExample.vcxproj /p:Configuration=Release /p:Platform=${{ matrix.platform }} /p:PlatformToolset=${{ matrix.toolset }} - name: Build DEBUG and RELEASE allAddonsExample working-directory: ${{env.GITHUB_WORKSPACE}} run: | - msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:Configuration=Debug /p:Platform=${{ matrix.platform }} /p:PlatformToolset=v145 - msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:Configuration=Release /p:Platform=${{ matrix.platform }} /p:PlatformToolset=v145 + msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:Configuration=Debug /p:Platform=${{ matrix.platform }} /p:PlatformToolset=${{ matrix.toolset }} + msbuild examples/templates/allAddonsExample/allAddonsExample.vcxproj /p:Configuration=Release /p:Platform=${{ matrix.platform }} /p:PlatformToolset=${{ matrix.toolset }} # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - rpi-build: runs-on: ubuntu-24.04 From 54930d380396c199938603fa4626732562546491 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 03:31:03 +1000 Subject: [PATCH 10/13] of Menu fixes for build all libraries --- scripts/of.sh | 184 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 49 deletions(-) diff --git a/scripts/of.sh b/scripts/of.sh index f6f2e17c7ce..29ad92a728e 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -573,6 +573,25 @@ printLibInlineSection(){ [[ "$n" -eq 0 ]] && printf ' %s—%s\n' "$C_MUTED" "$C_RESET" } +# Plain name list (no version lookup — addon dirs don't carry one the way +# bundled libs do). Used for the "Core Addons" section. +printNameListSection(){ + local title="$1" + shift + printf '\n' + printf ' %s%s%s %s────────────────%s\n' "$C_ACCENT" "$title" "$C_RESET" "$C_MUTED" "$C_RESET" + if [[ $# -eq 0 ]]; then + printf ' %s—%s\n' "$C_MUTED" "$C_RESET" + return 0 + fi + local line="" a + for a in "$@"; do + [[ -n "$line" ]] && line+=", " + line+="$a" + done + printf ' %s\n' "$line" +} + # bash-3.2 safe: track seen keys as newline-separated string _seenHas(){ [[ $'\n'"${_SEEN_KEYS}"$'\n' == *$'\n'"$1"$'\n'* ]] @@ -589,7 +608,9 @@ collectLibSections(){ _SEEN_KEYS="" local name path addon pair libname key - # core libs under libs/ + # libs/* — single pass classifying core / core-addon / other (was two + # passes over the same directory; halves the stat cost here, which + # matters on slower filesystems like Windows/MSYS2) if [[ -d "${OF_DIR}/libs" ]]; then for path in "${OF_DIR}/libs"/*/; do [[ -d "$path" ]] || continue @@ -600,8 +621,13 @@ collectLibSections(){ [[ -d "${path}include" || -d "${path}lib" ]] || continue if isCoreLibName "$name"; then SEC_CORE+=("$path") - _seenAdd "libs/$name" + elif isCoreAddonLibName "$name"; then + addon=$(coreAddonForLib "$name" 2>/dev/null || echo "addon") + SEC_CORE+=("${path}|${addon}") + else + SEC_OTHER+=("$path") fi + _seenAdd "libs/$name" done fi @@ -650,36 +676,12 @@ collectLibSections(){ _seenAdd "$key" done fi - - # others under libs/ - if [[ -d "${OF_DIR}/libs" ]]; then - for path in "${OF_DIR}/libs"/*/; do - [[ -d "$path" ]] || continue - name=$(basename "$path") - case "$name" in - download|openFrameworks|openFrameworksCompiled|scripts) continue ;; - esac - [[ -d "${path}include" || -d "${path}lib" ]] || continue - _seenHas "libs/$name" && continue - if isCoreLibName "$name"; then - continue - fi - if isCoreAddonLibName "$name"; then - addon=$(coreAddonForLib "$name" 2>/dev/null || echo "addon") - SEC_CORE+=("${path}|${addon}") - else - SEC_OTHER+=("$path") - fi - _seenAdd "libs/$name" - done - fi } cmdStatus(){ local ofVer pgBin pgVer lastUp hostLine local issues=0 local -a issueNotes=() - local total=0 printBanner "status" printf '\n' @@ -719,11 +721,13 @@ cmdStatus(){ fi taskSet 3 running - collectLibSections - total=$(( ${#SEC_CORE[@]} + ${#SEC_ADDONS[@]} + ${#SEC_OTHER[@]} )) + # assessLibsState alone is a handful of stat() calls (essentials only) — + # fast even on slow filesystems. The full libs/addons enumeration + # (collectLibSections) is much heavier (every libs/* and addons/*/libs/* + # dir) and runs later, right before it's printed, so it doesn't sit + # behind this spinner making status feel like it's hung. assessLibsState "$OF_PLATFORM" - # only flag libs if none detected - if [[ "$LIBS_STATE" == "missing" || "$total" -eq 0 ]]; then + if [[ "$LIBS_STATE" == "missing" ]]; then taskSet 3 fail "${LIBS_STATE_DETAIL:-none found}" issues=1 issueNotes+=("libraries missing") @@ -765,10 +769,21 @@ cmdStatus(){ skipped) echoKV "integrity" "SHA check skipped" ;; esac + # heavier enumeration deferred to here (see task-3 comment above) — every + # libs/* and addons/*/libs/* dir gets stat'd, which is the slow part on + # Windows/MSYS2, so it happens while printing rather than behind a spinner + collectLibSections printLibInlineSection "Core" "${SEC_CORE[@]}" printLibInlineSection "Addons" "${SEC_ADDONS[@]}" printLibInlineSection "Other" "${SEC_OTHER[@]}" + local -a coreAddonNames=() + local a + while IFS= read -r a; do + [[ -n "$a" ]] && coreAddonNames+=("$a") + done < <(listAddons) + printNameListSection "Core Addons" "${coreAddonNames[@]}" + printf '\n' if [[ "$issues" -eq 0 ]]; then echoSuccess "ready" @@ -830,6 +845,7 @@ printHelp(){ 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_APO_VS_VER=18 apothecary VS toolchain (18=VS2026, 17=VS2022, 16=VS2019) OF_JOBS=8 Parallel build jobs VERBOSE=1 NO_COLOR=1 OF_ANIM=0 @@ -880,6 +896,28 @@ cmdVersionPG(){ printf '\n' } +# Combined openFrameworks + Project Generator version screen (menu entry point) +menuVersion(){ + local ofVer bin ver + printBanner "version" + ofVer=$(readOfVersion) || ofVer="" + bin=$(findPGBinary) || bin="" + printf '\n' + echoKV "openFrameworks" "${ofVer:-—}" + echoKV "platform" "${OF_PLATFORM}${OF_ARCH:+ / ${OF_ARCH}}" + echoKV "cli" "$OF_SCRIPT_VERSION" + printf '\n' + if [[ -n "$bin" ]]; then + ver=$(readPGVersion) + echoKV "projectGenerator" "$ver" + echoKV "pg path" "$bin" + else + echoKV "projectGenerator" "not installed" + echoNote "try: of update pg" + fi + printf '\n' +} + OF_LIB_STATE_FILE="${OF_DIR}/libs/.of-cli-state" # Essentials used to decide "installed" @@ -1336,6 +1374,18 @@ archesForApoType(){ esac } +# Apothecary's own VS_VER env var picks the toolchain (17=VS2022, 18=VS2026, +# 16=VS2019) — if unset it silently defaults to 17 inside apothecary itself, +# regardless of what's actually installed. Default here to 18 (VS2026). +menuPickApoVsVer(){ + menuPick "Visual Studio version for apothecary build" \ + "Visual Studio 2026 (18)|18" \ + "Visual Studio 2022 (17)|17" \ + "Visual Studio 2019 (16)|16" \ + || return 1 + return 0 +} + runApothecaryEngine(){ local type="$1" arch="$2" shift 2 @@ -1350,7 +1400,13 @@ runApothecaryEngine(){ [[ "$VERBOSE" = 1 ]] && cmd+=( -v ) cmd+=( "$@" ) echoNote "${cmd[*]}" - ( cd "${APO_HOME}/apothecary" 2>/dev/null || cd "$APO_HOME" || exit 1; "${cmd[@]}" ) + if [[ "$type" == "vs" ]]; then + local vsVer="${OF_APO_VS_VER:-18}" + echoKV "VS_VER" "${vsVer} ($([[ "$vsVer" == 18 ]] && echo 2026 || { [[ "$vsVer" == 17 ]] && echo 2022 || echo 2019; }))" + ( cd "${APO_HOME}/apothecary" 2>/dev/null || cd "$APO_HOME" || exit 1; VS_VER="$vsVer" "${cmd[@]}" ) + else + ( cd "${APO_HOME}/apothecary" 2>/dev/null || cd "$APO_HOME" || exit 1; "${cmd[@]}" ) + fi } launchApoMenu(){ @@ -1416,7 +1472,13 @@ menuApothecary(){ choice="$UI_MENU_RESULT" case "$choice" in - build-host) cmdApothecaryBuildAll "$OF_PLATFORM" "$OF_ARCH" ;; + build-host) + if [[ "$OF_PLATFORM" == "vs" ]]; then + menuPickApoVsVer || return 0 + export OF_APO_VS_VER="$UI_MENU_RESULT" + fi + cmdApothecaryBuildAll "$OF_PLATFORM" "$OF_ARCH" + ;; build-type) opts=() for type in "${APO_BUILD_TYPES[@]}"; do @@ -1431,6 +1493,10 @@ menuApothecary(){ [[ "$OF_PLATFORM" == "osx" ]] && echoNote "macos covers osx · ios · tvos · xros · watchos · catos" menuPick "Build platform" "${opts[@]}" || return 0 type="$UI_MENU_RESULT" + if [[ "$type" == "vs" ]]; then + menuPickApoVsVer || return 0 + export OF_APO_VS_VER="$UI_MENU_RESULT" + fi aopts=() read -r -a arches <<< "$(archesForApoType "$type")" for a in "${arches[@]}"; do @@ -1446,6 +1512,10 @@ menuApothecary(){ done menuPick "Platform" "${opts[@]}" || return 0 type="$UI_MENU_RESULT" + if [[ "$type" == "vs" ]]; then + menuPickApoVsVer || return 0 + export OF_APO_VS_VER="$UI_MENU_RESULT" + fi aopts=() read -r -a arches <<< "$(archesForApoType "$type")" for a in "${arches[@]}"; do aopts+=("${a}|${a}"); done @@ -1503,6 +1573,26 @@ cmdUpgrade(){ tasksSummary } +# Single menu entry point for "Upgrade" — old-layout projects/addons +# (pre-0.12 linux64 → linux/64 path style) up to the current SDK version. +menuUpgrade(){ + local ofVer choice + ofVer=$(readOfVersion) || ofVer="" + menuPick "Upgrade — oF 7.0–11.0 → ${ofVer:+v${ofVer} }(Latest SDK)" \ + "Addons only|addons" \ + "Projects (apps/)|apps" \ + "Both|both" \ + "Back|back" \ + || return 0 + choice="$UI_MENU_RESULT" + case "$choice" in + addons) cmdUpgrade addons ;; + apps) cmdUpgrade apps ;; + both) cmdUpgrade addons; cmdUpgrade apps ;; + back) return 0 ;; + esac +} + cmdSetup(){ local platformDir="${1:-$OF_PLATFORM}" local distroDir depsScript codecsScript @@ -2152,10 +2242,8 @@ cmdMenu(){ "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" \ - "Upgrade apps|upgrade-apps" \ + "Version info — openFrameworks + Project Generator|version" \ + "Upgrade — Projects / Addons…|upgrade" \ "Help|help" \ "Quit|quit" then @@ -2165,19 +2253,17 @@ cmdMenu(){ choice="$UI_MENU_RESULT" printf '\n' case "$choice" in - 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 ;; - upgrade-apps) cmdUpgrade apps; menuPause ;; - help) printHelp; menuPause ;; - quit) echoSuccess "bye"; return 0 ;; - *) echoError "unknown: $choice"; menuPause ;; + status) cmdStatus; menuPause ;; + setup) cmdSetup "$OF_PLATFORM"; menuPause ;; + update) menuUpdate; menuPause ;; + build) menuBuild ;; + apothecary) menuApothecary; menuPause ;; + cleanup|clean) menuCleanup ;; + version) menuVersion; menuPause ;; + upgrade) menuUpgrade; menuPause ;; + help) printHelp; menuPause ;; + quit) echoSuccess "bye"; return 0 ;; + *) echoError "unknown: $choice"; menuPause ;; esac done } From f6080a079f0072f3f82c47c9b309d29529419222 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 13:24:52 +1000 Subject: [PATCH 11/13] of Menu 0.4.2 - Fixes for setup(), update(), draw(), exit() functions for gum --- scripts/of.sh | 64 +++++++++++++++++++++++---------------------- scripts/of_build.sh | 12 +++++++++ 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/scripts/of.sh b/scripts/of.sh index 29ad92a728e..86aafb4fe85 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.4.0 +OF_SCRIPT_VERSION=0.4.2 OF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" OF_DIR="$(realpath "$OF_DIR/../")" @@ -1468,13 +1468,16 @@ menuApothecary(){ ) [[ -n "$APO_CLI" ]] && opts+=("Open Apothecary interactive menu|apo-menu") opts+=("Status|status" "Back|back") - menuPick "Apothecary builds" "${opts[@]}" || return 0 + # return 2 (not 0) for every "nothing happened, back/cancelled" exit below + # so the caller can skip the "press Enter to return" pause and go straight + # back up a level instead of pausing on an empty/unchanged screen. + menuPick "Apothecary builds" "${opts[@]}" || return 2 choice="$UI_MENU_RESULT" case "$choice" in build-host) if [[ "$OF_PLATFORM" == "vs" ]]; then - menuPickApoVsVer || return 0 + menuPickApoVsVer || return 2 export OF_APO_VS_VER="$UI_MENU_RESULT" fi cmdApothecaryBuildAll "$OF_PLATFORM" "$OF_ARCH" @@ -1491,10 +1494,10 @@ menuApothecary(){ fi done [[ "$OF_PLATFORM" == "osx" ]] && echoNote "macos covers osx · ios · tvos · xros · watchos · catos" - menuPick "Build platform" "${opts[@]}" || return 0 + menuPick "Build platform" "${opts[@]}" || return 2 type="$UI_MENU_RESULT" if [[ "$type" == "vs" ]]; then - menuPickApoVsVer || return 0 + menuPickApoVsVer || return 2 export OF_APO_VS_VER="$UI_MENU_RESULT" fi aopts=() @@ -1502,7 +1505,7 @@ menuApothecary(){ for a in "${arches[@]}"; do aopts+=("${a}|${a}") done - menuPick "Architecture · ${type}" "${aopts[@]}" || return 0 + menuPick "Architecture · ${type}" "${aopts[@]}" || return 2 cmdApothecaryBuildAll "$type" "$UI_MENU_RESULT" ;; build-one) @@ -1510,16 +1513,16 @@ menuApothecary(){ for type in "${APO_BUILD_TYPES[@]}"; do opts+=("${type}|${type}") done - menuPick "Platform" "${opts[@]}" || return 0 + menuPick "Platform" "${opts[@]}" || return 2 type="$UI_MENU_RESULT" if [[ "$type" == "vs" ]]; then - menuPickApoVsVer || return 0 + menuPickApoVsVer || return 2 export OF_APO_VS_VER="$UI_MENU_RESULT" fi aopts=() read -r -a arches <<< "$(archesForApoType "$type")" for a in "${arches[@]}"; do aopts+=("${a}|${a}"); done - menuPick "Architecture · ${type}" "${aopts[@]}" || return 0 + menuPick "Architecture · ${type}" "${aopts[@]}" || return 2 arch="$UI_MENU_RESULT" opts=() local f @@ -1529,9 +1532,9 @@ menuApothecary(){ if [[ -d "$f" ]]; then opts+=("${name}|${name}") elif [[ "$name" == *.sh ]]; then opts+=("${name%.sh}|${name%.sh}"); fi done - menuPick "Formula · ${type}/${arch}" "${opts[@]}" || return 0 + menuPick "Formula · ${type}/${arch}" "${opts[@]}" || return 2 name="$UI_MENU_RESULT" - confirmYes "Build ${name} for ${type}/${arch}?" || return 0 + confirmYes "Build ${name} for ${type}/${arch}?" || return 2 tasksBegin "Tasks" "Resolve" "Build ${name}" "Finish" taskSet 0 done "$APO_HOME" if ! taskLive 1 -- runApothecaryEngine "$type" "$arch" update "$name"; then @@ -1550,7 +1553,7 @@ menuApothecary(){ echoKV "libs out" "${OF_DIR}/libs" printf '\n' ;; - back) return 0 ;; + back) return 2 ;; esac } @@ -1583,13 +1586,13 @@ menuUpgrade(){ "Projects (apps/)|apps" \ "Both|both" \ "Back|back" \ - || return 0 + || return 2 choice="$UI_MENU_RESULT" case "$choice" in - addons) cmdUpgrade addons ;; - apps) cmdUpgrade apps ;; + addons) cmdUpgrade addons; return $? ;; + apps) cmdUpgrade apps; return $? ;; both) cmdUpgrade addons; cmdUpgrade apps ;; - back) return 0 ;; + back) return 2 ;; esac } @@ -2217,13 +2220,16 @@ menuCleanup(){ } cmdMenu(){ - local choice setupLabel="Setup — libs + Project Generator (apothecary @ latest)" + # labelled like the ofBaseApp lifecycle (setup/update/draw/exit) since + # this is openFrameworks' own menu — fn name shown dim on the right + local choice + local setupDesc="libs + Project Generator (apothecary @ latest)" if ! menuCanRun; then echoWarning "no TTY — showing status" cmdStatus return 0 fi - [[ "$OF_PLATFORM" == "linux" ]] && setupLabel="Setup — distro deps + libs + Project Generator" + [[ "$OF_PLATFORM" == "linux" ]] && setupDesc="distro deps + libs + Project Generator" while true; do printf '\n' @@ -2236,16 +2242,14 @@ cmdMenu(){ printf '\n' if ! menuPick "What do you want to do?" \ - "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" \ - "Version info — openFrameworks + Project Generator|version" \ - "Upgrade — Projects / Addons…|upgrade" \ - "Help|help" \ - "Quit|quit" + "status() — system checker (cmdStatus)|status" \ + "setup() — ${setupDesc} (cmdSetup)|setup" \ + "update() — libs / PG (choose source) (menuUpdate)|update" \ + "draw() — build: core / apothecary / upgrade / examples / cmake (menuBuild)|build" \ + "cleanup() — projects / caches / libraries (menuCleanup)|cleanup" \ + "version() — openFrameworks + Project Generator (menuVersion)|version" \ + "help() (printHelp)|help" \ + "exit()|exit" then echoInfo "bye" return 0 @@ -2257,12 +2261,10 @@ cmdMenu(){ setup) cmdSetup "$OF_PLATFORM"; menuPause ;; update) menuUpdate; menuPause ;; build) menuBuild ;; - apothecary) menuApothecary; menuPause ;; cleanup|clean) menuCleanup ;; version) menuVersion; menuPause ;; - upgrade) menuUpgrade; menuPause ;; help) printHelp; menuPause ;; - quit) echoSuccess "bye"; return 0 ;; + exit|quit) echoSuccess "bye"; return 0 ;; *) echoError "unknown: $choice"; menuPause ;; esac done diff --git a/scripts/of_build.sh b/scripts/of_build.sh index 6fdd7197a83..05edfe7f520 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -1112,6 +1112,8 @@ menuBuild(){ "Generate project files (PG)…|generate" \ "CMake…|cmake" \ "Clean project…|clean" \ + "Apothecary — build libraries…|apothecary" \ + "Upgrade — Projects / Addons…|upgrade" \ "Help|help" \ "Back|back" \ || return 0 @@ -1247,6 +1249,16 @@ menuBuild(){ runCleanProject "$project" menuPause ;; + apothecary) + menuApothecary + # menuApothecary returns 2 for "back / cancelled, nothing ran" — + # skip the pause then instead of stopping on an empty screen + [[ $? -eq 2 ]] || menuPause + ;; + upgrade) + menuUpgrade + [[ $? -eq 2 ]] || menuPause + ;; help) printHelpBuild menuPause From bc8b6185bc4010d5c173caf78d827d1642b8d56d Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 14:29:47 +1000 Subject: [PATCH 12/13] of Menu cleanup of status for addons / libraries / menu prompt --- scripts/of.sh | 286 ++++++++++++++++++++++++++++---------------- scripts/of_build.sh | 5 +- 2 files changed, 186 insertions(+), 105 deletions(-) diff --git a/scripts/of.sh b/scripts/of.sh index 86aafb4fe85..d954367bd45 100755 --- a/scripts/of.sh +++ b/scripts/of.sh @@ -46,6 +46,25 @@ OF_CORE_ADDON_MAP=( oscpack:ofxOsc ) +# All addons that ship with vanilla openFrameworks — broader than +# OF_CORE_ADDON_MAP (which only covers addons bundling a 3rd-party lib); +# most core addons (ofxGui, ofxNetwork, ofxXmlSettings, …) are pure C++ / +# OS-framework wrappers with nothing to download, so they'd otherwise never +# show up anywhere in `of status`. +OF_CORE_ADDON_NAMES=( + ofxAccelerometer ofxAndroid ofxAssimp ofxAssimpModelLoader ofxEmscripten + ofxGPS ofxGui ofxiOS ofxKinect ofxNetwork ofxOpenCv ofxOsc ofxPoco ofxSvg + ofxThreadedImageLoader ofxUnitTests ofxVectorGraphics ofxXmlSettings +) + +isCoreAddonName(){ + local n="$1" x + for x in "${OF_CORE_ADDON_NAMES[@]}"; do + [[ "$x" == "$n" ]] && return 0 + done + return 1 +} + APO_HOME="" APO_ENGINE="" APO_CLI="" @@ -525,71 +544,148 @@ readLibVersionMeta(){ return 1 } -# Streams "name (version · note)" one line at a time as each library is read, -# instead of resolving the whole section silently before printing anything. -# Version lookup does several `find` passes per lib (pkl/pkgconfig/cmake/header -# fallbacks) which can be slow on Windows/MSYS2 filesystems — this keeps the -# status command visibly moving instead of appearing to hang. +# Streams "name(version · note), name(version · note), …" onto one growing +# line as each library is read, instead of resolving the whole section +# silently before printing anything. Version lookup does several `find` +# passes per lib (pkl/pkgconfig/cmake/header fallbacks) which can be slow on +# Windows/MSYS2 filesystems — this keeps the status command visibly moving +# (each entry appears the moment it's read) instead of appearing to hang. printLibInlineSection(){ local title="$1" shift + # nothing to show — skip the whole section instead of a header + "—" + [[ $# -eq 0 ]] && return 0 + printf '\n' printf ' %s%s%s %s────────────────%s\n' "$C_ACCENT" "$title" "$C_RESET" "$C_MUTED" "$C_RESET" - if [[ $# -eq 0 ]]; then - printf ' %s—%s\n' "$C_MUTED" "$C_RESET" - return 0 - fi local -a items=("$@") local path name note entry n=0 + # group entries by their addon note first (ofxEmscripten(html5audio 1.0.0, + # html5video 1.0.0)) instead of repeating the addon name per lib; entries + # with no note (plain core libs) print individually same as before + local -a noteOrder=() + local seenNotes=" " + for path in "${items[@]}"; do + note="" + [[ "$path" == *"|"* ]] && note="${path#*|}" + [[ -n "$note" && "$seenNotes" != *" ${note} "* ]] && { noteOrder+=("$note"); seenNotes+="${note} "; } + done + + printf ' ' + local grp gi + for grp in "${noteOrder[@]}"; do + [[ "$n" -gt 0 ]] && printf '%s,%s ' "$C_MUTED" "$C_RESET" + printf '%s%s%s(' "$C_FG" "$grp" "$C_RESET" + gi=0 + for path in "${items[@]}"; do + note="" + if [[ "$path" == *"|"* ]]; then + note="${path#*|}" + path="${path%%|*}" + fi + [[ "$note" == "$grp" ]] || continue + [[ -d "$path" ]] || continue + name=$(basename "$path") + if readLibVersionMeta "$path"; then + entry="${name} ${LIB_META_VER}" + else + entry="${name} ?" + fi + [[ "$gi" -gt 0 ]] && printf '%s,%s ' "$C_MUTED" "$C_RESET" + printf '%s%s%s' "$C_FG" "$entry" "$C_RESET" + gi=$((gi + 1)) + done + printf '%s)%s' "$C_FG" "$C_RESET" + n=$((n + 1)) + done + for path in "${items[@]}"; do note="" if [[ "$path" == *"|"* ]]; then note="${path#*|}" path="${path%%|*}" fi + [[ -n "$note" ]] && continue [[ -d "$path" ]] || continue name=$(basename "$path") - - if isInteractive; then - printf ' %s·%s %s%s%s' "$C_MUTED" "$C_RESET" "$C_FG" "$name" "$C_RESET" - fi - if readLibVersionMeta "$path"; then - entry="${name} (${LIB_META_VER}${note:+ · ${note}})" - else - entry="${name} (?${note:+ · ${note}})" - fi - - if isInteractive; then - printf '\r\033[K %s·%s %s\n' "$C_MUTED" "$C_RESET" "$entry" + entry="${name}(${LIB_META_VER})" else - printf ' · %s\n' "$entry" + entry="${name}(?)" fi + [[ "$n" -gt 0 ]] && printf '%s,%s ' "$C_MUTED" "$C_RESET" + printf '%s%s%s' "$C_FG" "$entry" "$C_RESET" n=$((n + 1)) done - [[ "$n" -eq 0 ]] && printf ' %s—%s\n' "$C_MUTED" "$C_RESET" + [[ "$n" -eq 0 ]] && printf '%s—%s' "$C_MUTED" "$C_RESET" + printf '\n' } -# Plain name list (no version lookup — addon dirs don't carry one the way -# bundled libs do). Used for the "Core Addons" section. -printNameListSection(){ +# Every addon name in $names shown as "name(lib version, …)" when it has +# bundled libs among $items (path|note entries, same shape as SEC_CORE / +# SEC_ADDONS), or plain "name" when it doesn't — most core addons (ofxGui, +# ofxNetwork, …) have no bundled 3rd-party lib at all. Args: title, names..., +# "--", items... +printAddonGroupSection(){ local title="$1" shift + + local -a names=() items=() + local sawSep=0 a + for a in "$@"; do + if [[ "$sawSep" -eq 0 && "$a" == "--" ]]; then + sawSep=1 + continue + fi + if [[ "$sawSep" -eq 0 ]]; then + names+=("$a") + else + items+=("$a") + fi + done + + # nothing to show — skip the whole section instead of a header + "—" + [[ ${#names[@]} -eq 0 ]] && return 0 + printf '\n' printf ' %s%s%s %s────────────────%s\n' "$C_ACCENT" "$title" "$C_RESET" "$C_MUTED" "$C_RESET" - if [[ $# -eq 0 ]]; then - printf ' %s—%s\n' "$C_MUTED" "$C_RESET" - return 0 - fi - local line="" a - for a in "$@"; do - [[ -n "$line" ]] && line+=", " - line+="$a" + printf ' ' + local nm path note libname n=0 gi + local -a libEntries + for nm in "${names[@]}"; do + [[ "$n" -gt 0 ]] && printf '%s,%s ' "$C_MUTED" "$C_RESET" + libEntries=() + for path in "${items[@]}"; do + note="" + if [[ "$path" == *"|"* ]]; then + note="${path#*|}" + path="${path%%|*}" + fi + [[ "$note" == "$nm" ]] || continue + [[ -d "$path" ]] || continue + libname=$(basename "$path") + if readLibVersionMeta "$path"; then + libEntries+=("${libname} ${LIB_META_VER}") + else + libEntries+=("${libname} ?") + fi + done + if [[ ${#libEntries[@]} -gt 0 ]]; then + printf '%s%s%s(' "$C_FG" "$nm" "$C_RESET" + for ((gi = 0; gi < ${#libEntries[@]}; gi++)); do + [[ "$gi" -gt 0 ]] && printf '%s,%s ' "$C_MUTED" "$C_RESET" + printf '%s%s%s' "$C_FG" "${libEntries[$gi]}" "$C_RESET" + done + printf '%s)%s' "$C_FG" "$C_RESET" + else + printf '%s%s%s' "$C_FG" "$nm" "$C_RESET" + fi + n=$((n + 1)) done - printf ' %s\n' "$line" + printf '\n' } # bash-3.2 safe: track seen keys as newline-separated string @@ -773,16 +869,39 @@ cmdStatus(){ # libs/* and addons/*/libs/* dir gets stat'd, which is the slow part on # Windows/MSYS2, so it happens while printing rather than behind a spinner collectLibSections - printLibInlineSection "Core" "${SEC_CORE[@]}" - printLibInlineSection "Addons" "${SEC_ADDONS[@]}" - printLibInlineSection "Other" "${SEC_OTHER[@]}" - local -a coreAddonNames=() + # SEC_CORE mixes two things: plain core libs under libs/ (no note), and + # libs bundled specifically for one of OF's own core addons (note = addon + # name, e.g. assimp for ofxAssimpModelLoader). Split them into their own + # sections instead of showing them side by side under one "Core" header. + local -a coreLibsOnly=() + local p + for p in "${SEC_CORE[@]}"; do + [[ "$p" == *"|"* ]] || coreLibsOnly+=("$p") + done + + # every installed addon, split into core vs other by name (not by whether + # it happens to bundle a 3rd-party lib — most core addons don't) + local -a installedAddons=() coreAddonNames=() otherAddonNames=() local a while IFS= read -r a; do - [[ -n "$a" ]] && coreAddonNames+=("$a") + [[ -n "$a" ]] && installedAddons+=("$a") done < <(listAddons) - printNameListSection "Core Addons" "${coreAddonNames[@]}" + for a in "${installedAddons[@]}"; do + if isCoreAddonName "$a"; then + coreAddonNames+=("$a") + else + otherAddonNames+=("$a") + fi + done + + printLibInlineSection "Core (Libraries)" "${coreLibsOnly[@]}" + # search both pools regardless of which one collectLibSections happened to + # put a given addon's libs into (its own addon-name list doesn't + # necessarily match OF_CORE_ADDON_NAMES, e.g. ofxEmscripten) + printAddonGroupSection "Core (Addons)" "${coreAddonNames[@]}" -- "${SEC_CORE[@]}" "${SEC_ADDONS[@]}" + printAddonGroupSection "Other Addons — every other addon under addons/" "${otherAddonNames[@]}" -- "${SEC_CORE[@]}" "${SEC_ADDONS[@]}" + printLibInlineSection "Other Libraries — libs/ not part of OF's core set" "${SEC_OTHER[@]}" printf '\n' if [[ "$issues" -eq 0 ]]; then @@ -1284,51 +1403,20 @@ menuPickOfLibs(){ esac } -menuPickPlatformForLibs(){ - local -a opts=() - local p script label - for p in "${OF_LIB_PLATFORMS[@]}"; do - script="${OF_CORE_SCRIPT_DIR}/${p}/download_libs.sh" - if [[ "$LIB_SOURCE" != "apothecary" ]] || [[ -f "$script" ]]; then - label="$p" - if [[ "$LIB_SOURCE" == "apothecary" ]]; then - case "$p" in - macos) label="macos — Apple multi-target (osx · ios · tvos · xros · watchos · catos)" ;; - osx) - label="osx — desktop host package" - [[ "$p" == "$OF_PLATFORM" ]] && label+=" (this machine)" - ;; - *) [[ "$p" == "$OF_PLATFORM" ]] && label="${p} (this machine)" ;; - esac - else - [[ "$p" == "$OF_PLATFORM" ]] && label="${p} (this machine)" - fi - opts+=("${label}|${p}") - fi - done - [[ "$LIB_SOURCE" == "apothecary" && "$OF_PLATFORM" == "osx" ]] && \ - echoNote "macos packages install lib/macos/*.xcframework covering all Apple targets" - menuPick "Target platform" "${opts[@]}" || return 1 -} - menuDownloadLibs(){ local platformDir="${1:-$OF_PLATFORM}" printBanner "libs" echoInfo "download libraries · choose source" printf '\n' - menuPickLibSource || return 1 + menuPickLibSource || return 2 echoSuccess "source → $LIB_SOURCE" - menuPickLibTag "$LIB_SOURCE" || return 1 + menuPickLibTag "$LIB_SOURCE" || return 2 echoSuccess "tag → $LIB_TAG" - if [[ "$platformDir" == "$OF_PLATFORM" ]]; then - if menuCanRun && confirmNo "Download for another platform instead of ${platformDir}?"; then - menuPickPlatformForLibs || return 1 - platformDir="$UI_MENU_RESULT" - fi - fi - + # source + tag is enough to start — no extra gates after this. Cross- + # platform downloads and non-default clean modes are still available via + # `of update libs ` / LIB_CLEAN_MODE for the cases that need them. if [[ "$LIB_SOURCE" == "apothecary" ]]; then case "$platformDir" in osx) @@ -1339,22 +1427,10 @@ 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 + export LIB_CLEAN_MODE="${LIB_CLEAN_MODE:-platform}" + echoKV "clean" "$LIB_CLEAN_MODE" fi - [[ "$LIB_SOURCE" == "oflibs" ]] && { menuPickOfLibs "$LIB_TAG" "$platformDir" || return 1; } + [[ "$LIB_SOURCE" == "oflibs" ]] && { menuPickOfLibs "$LIB_TAG" "$platformDir" || return 2; } # force download after explicit update path (user already chose source) cmdUpdateLibs "$platformDir" 1 @@ -2241,14 +2317,15 @@ cmdMenu(){ [[ -n "$OF_LINUX_DISTRO" ]] && echoKV "distro" "$OF_LINUX_DISTRO" printf '\n' + # short labels on purpose — long lines wrap/truncate differently across + # terminals and can desync gum's highlighted row from the real selection if ! menuPick "What do you want to do?" \ - "status() — system checker (cmdStatus)|status" \ - "setup() — ${setupDesc} (cmdSetup)|setup" \ - "update() — libs / PG (choose source) (menuUpdate)|update" \ - "draw() — build: core / apothecary / upgrade / examples / cmake (menuBuild)|build" \ - "cleanup() — projects / caches / libraries (menuCleanup)|cleanup" \ - "version() — openFrameworks + Project Generator (menuVersion)|version" \ - "help() (printHelp)|help" \ + "status() — checker (cmdStatus)|status" \ + "setup() — install (cmdSetup)|setup" \ + "update() — refresh (menuUpdate)|update" \ + "draw() — build (menuBuild)|build" \ + "cleanup() — free space (menuCleanup)|cleanup" \ + "version() — info (menuVersion)|version" \ "exit()|exit" then echoInfo "bye" @@ -2259,11 +2336,10 @@ cmdMenu(){ case "$choice" in status) cmdStatus; menuPause ;; setup) cmdSetup "$OF_PLATFORM"; menuPause ;; - update) menuUpdate; menuPause ;; + update) menuUpdate; [[ $? -eq 2 ]] || menuPause ;; build) menuBuild ;; cleanup|clean) menuCleanup ;; version) menuVersion; menuPause ;; - help) printHelp; menuPause ;; exit|quit) echoSuccess "bye"; return 0 ;; *) echoError "unknown: $choice"; menuPause ;; esac @@ -2282,22 +2358,24 @@ menuUpdate(){ echoKV "pg" "${PG_STATE} — ${PG_STATE_DETAIL}" printf '\n' + # return 2 for "nothing happened, back/cancelled" so the caller can skip + # the "press Enter to return" pause, same convention as menuApothecary/menuUpgrade menuPick "What to update?" \ "Libraries (choose source)…|libs" \ "Project Generator|pg" \ "Libraries + Project Generator|all" \ "Back|back" \ - || return 0 + || return 2 what="$UI_MENU_RESULT" case "$what" in - libs) menuDownloadLibs "$OF_PLATFORM" ;; + libs) menuDownloadLibs "$OF_PLATFORM"; return $? ;; pg) cmdUpdatePG "$OF_PLATFORM" 1 ;; all) menuDownloadLibs "$OF_PLATFORM" || return $? # after libs, refresh PG to latest cmdUpdatePG "$OF_PLATFORM" 1 ;; - back) return 0 ;; + back) return 2 ;; esac } diff --git a/scripts/of_build.sh b/scripts/of_build.sh index 05edfe7f520..a0324c8114e 100644 --- a/scripts/of_build.sh +++ b/scripts/of_build.sh @@ -153,11 +153,14 @@ listAppGroups(){ done | sort } +# Every addon dir under addons/ — core ofx* ones and any other/ofxAddons-style +# folder that doesn't follow the ofx naming convention listAddons(){ local d name - for d in "${OF_DIR}/addons"/ofx*/; do + for d in "${OF_DIR}/addons"/*/; do [[ -d "$d" ]] || continue name=$(basename "$d") + [[ "$name" == .* ]] && continue printf '%s\n' "$name" done | sort } From a3dd1e7d1b657775bd2a5e83ec90a64f5afa1aad Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 10 Aug 2026 15:35:24 +1000 Subject: [PATCH 13/13] smoke test nightly --- scripts/dev/smoke_test_nightly.sh | 250 ++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100755 scripts/dev/smoke_test_nightly.sh diff --git a/scripts/dev/smoke_test_nightly.sh b/scripts/dev/smoke_test_nightly.sh new file mode 100755 index 00000000000..d85ff598362 --- /dev/null +++ b/scripts/dev/smoke_test_nightly.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# smoke_test_nightly.sh — logical smoke test for whether this branch's +# download_libs / install_dependencies / of.yml changes would break a +# nightly or CI build. +# +# Runs in an isolated `git worktree` copy of the repo — never touches your +# real working checkout, so it's safe to run repeatedly and to Ctrl-C. +# +# Sections: +# 1. Syntax check every script changed on this branch vs master +# 2. --help sanity for each platform's download_libs.sh (no network) +# 3. .github/workflows/of.yml still parses, build-vs matrix expands to 6 jobs +# 4. gum block in scripts/linux/*/install_dependencies.sh can't abort the +# script even when gum isn't in apt/dnf (mocked apt-cache/dnf, no root/network) +# 5. [network, opt-in] a REAL download_libs.sh run mirroring nightly.yml's +# exact invocation, in the isolated worktree +# 6. [needs 5] SHA-256 verify state gets written, `of.sh status` reports it +# cleanly, and cleanLibraries' git-tracked check doesn't protect the +# freshly-downloaded (untracked) libs from a real cleanup +# +# Usage: +# scripts/dev/smoke_test_nightly.sh # sections 1-4 only (fast, offline) +# scripts/dev/smoke_test_nightly.sh --real # also run 5-6 (network, real download) +# scripts/dev/smoke_test_nightly.sh --real --keep # ...and leave the worktree for inspection + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OF_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +DO_REAL=0 +KEEP=0 +for arg in "$@"; do + case "$arg" in + --real) DO_REAL=1 ;; + --keep) KEEP=1 ;; + esac +done + +PASS=0 +FAIL=0 +pass(){ printf ' \033[32m\xE2\x9C\x93\033[0m %s\n' "$1"; PASS=$((PASS + 1)); } +fail(){ printf ' \033[31m\xE2\x9C\x97\033[0m %s\n' "$1"; FAIL=$((FAIL + 1)); } +section(){ printf '\n\033[1m%s\033[0m\n' "$1"; } + +cd "$OF_DIR" + +# --------------------------------------------------------------------------- +section "1. Syntax check — scripts changed on this branch vs master" +# --------------------------------------------------------------------------- +MB=$(git merge-base HEAD master 2>/dev/null || true) +CHANGED_SH=() +if [[ -n "$MB" ]]; then + while IFS= read -r f; do + [[ -n "$f" ]] && CHANGED_SH+=("$f") + done < <(git diff --name-only "$MB" -- 'scripts/**/*.sh' 'scripts/*.sh' 2>/dev/null) +fi +if [[ ${#CHANGED_SH[@]} -eq 0 ]]; then + fail "could not compute changed-file list from git — falling back to a known list" + CHANGED_SH=( + scripts/of.sh scripts/of_build.sh scripts/ui.sh scripts/dev/upgrade.sh + scripts/dev/download_libs.sh scripts/dev/lib_sources.sh scripts/dev/sha_verify.sh + scripts/osx/install_dependencies.sh scripts/vs/install_dependencies.sh + scripts/linux/ubuntu/install_dependencies.sh scripts/linux/debian/install_dependencies.sh + scripts/linux/fedora/install_dependencies.sh scripts/linux/archlinux/install_dependencies.sh + scripts/linux/download_libs.sh scripts/android/download_libs.sh scripts/emscripten/download_libs.sh + scripts/ios/download_libs.sh scripts/macos/download_libs.sh scripts/msys2/download_libs.sh + scripts/osx/download_libs.sh scripts/vs/download_libs.sh scripts/msys2/install_dependencies.sh + ) +fi +for f in "${CHANGED_SH[@]}"; do + [[ -f "$f" ]] || continue + err=$(bash -n "$f" 2>&1) && pass "$f" || fail "$f -- $err" +done + +# --------------------------------------------------------------------------- +section "2. download_libs.sh --help sanity (no network) for each platform" +# --------------------------------------------------------------------------- +for p in linux osx macos vs msys2 android emscripten ios; do + f="scripts/${p}/download_libs.sh" + [[ -f "$f" ]] || continue + if out=$(bash "$f" --help 2>&1); then + pass "$f --help" + else + # some wrappers exit non-zero on --help by forwarding to dev/download_libs.sh's + # own exit 0 after printHelp -- only fail if it looks like a real crash + if echo "$out" | grep -qi "usage\|help"; then + pass "$f --help (non-zero exit but help text printed — likely fine)" + else + fail "$f --help -- $out" + fi + fi +done + +# --------------------------------------------------------------------------- +section "3. .github/workflows/of.yml — YAML validity + build-vs matrix" +# --------------------------------------------------------------------------- +if command -v python3 >/dev/null 2>&1 && python3 -c "import yaml" 2>/dev/null; then + result=$(python3 - <<'PY' +import yaml, sys +d = yaml.safe_load(open(".github/workflows/of.yml")) +job = d["jobs"].get("build-vs") +if not job: + print("MISSING build-vs job"); sys.exit(1) +m = job["strategy"]["matrix"] +n = len(m.get("platform", [])) * len(m.get("vsYear", [])) +inc = m.get("include", []) +if n != 6: + print(f"expected 6 platform x vsYear combos, got {n}"); sys.exit(1) +years = {i["vsYear"] for i in inc} +if years != {2022, 2026}: + print(f"include entries don't cover vsYear 2022+2026: {years}"); sys.exit(1) +print("OK") +PY +) + if [[ "$result" == "OK" ]]; then + pass "of.yml parses; build-vs matrix expands to 6 jobs (3 platforms x 2 VS years)" + else + fail "of.yml -- $result" + fi +else + fail "python3 + pyyaml not available — skipped (install with: pip3 install pyyaml)" +fi + +# --------------------------------------------------------------------------- +section "4. gum block can't abort scripts/linux/*/install_dependencies.sh" +# --------------------------------------------------------------------------- +# Mocks apt-cache/dnf so "gum not found" is exercised without root or network, +# matching what actually happens on GitHub's ubuntu-24.04 runners today. +MOCKDIR=$(mktemp -d) +cat > "$MOCKDIR/apt-cache" <<'EOF' +#!/usr/bin/env bash +[[ "$2" == "gum" ]] && exit 1 # simulate: gum not in configured repos +exit 0 +EOF +cat > "$MOCKDIR/dnf" <<'EOF' +#!/usr/bin/env bash +exit 1 # simulate: gum not available +EOF +chmod +x "$MOCKDIR/apt-cache" "$MOCKDIR/dnf" + +for distro in ubuntu debian fedora; do + f="scripts/linux/${distro}/install_dependencies.sh" + [[ -f "$f" ]] || continue + # extract just the gum block in isolation so we're not trying to fake + # an entire apt/dnf install run (needs root + hundreds of real packages) + block=$(awk '/# Optional: gum/{flag=1} flag{print} /^fi$/{if(flag){exit}}' "$f") + if [[ -z "$block" ]]; then + fail "$f -- could not find the gum block (marker comment missing?)" + continue + fi + out=$(PATH="$MOCKDIR:$PATH" bash -c "$block" 2>&1) + code=$? + if [[ $code -eq 0 ]]; then + pass "$f gum block exits 0 when gum is unavailable" + else + fail "$f gum block exited $code -- $out" + fi +done +rm -rf "$MOCKDIR" + +# --------------------------------------------------------------------------- +if [[ "$DO_REAL" -eq 0 ]]; then + section "Skipped (pass --real to also run): a real download_libs.sh run + of.sh status + cleanLibraries check" +else + section "5. REAL download_libs.sh run (network!), mirroring nightly.yml's linux64 job" + WT_DIR=$(mktemp -d)/of-smoke-test + CUR_SHA=$(git rev-parse HEAD) + # --detach: HEAD's branch is already checked out in the main tree, and + # git refuses to check out the same branch in two worktrees at once + if git worktree add --detach "$WT_DIR" "$CUR_SHA" >/tmp/smoke_wt.log 2>&1; then + pass "created isolated worktree at $WT_DIR (detached at $CUR_SHA)" + else + fail "git worktree add failed -- $(cat /tmp/smoke_wt.log)" + exit 1 + fi + + ( + cd "$WT_DIR" || exit 1 + echo " -- running: scripts/linux/download_libs.sh -a 64gcc6 (same as nightly.yml) --" + if ./scripts/linux/download_libs.sh -a 64gcc6; then + echo PASS_DOWNLOAD + else + echo "FAIL_DOWNLOAD exit=$?" + fi + ) | tee /tmp/smoke_download.log + if grep -q PASS_DOWNLOAD /tmp/smoke_download.log; then + pass "download_libs.sh -a 64gcc6 completed" + else + fail "download_libs.sh -a 64gcc6 -- see /tmp/smoke_download.log" + fi + + section "6. of.sh status + cleanLibraries sanity against the freshly downloaded libs" + # force PLATFORM=linux: we downloaded linux64 libs above (matching + # nightly.yml) regardless of what host we're actually running this on. + # cmdStatus intentionally exits non-zero when setup is incomplete (no + # Project Generator here, since we only ran download_libs) -- that's + # correct behavior, not a bug, so check for a clean run (no crash/bash + # error), not exit code 0. + (cd "$WT_DIR" && PLATFORM=linux bash scripts/of.sh status < /dev/null) > /tmp/smoke_status.log 2>&1 + if grep -qE "unbound variable|command not found|syntax error|: line [0-9]+:" /tmp/smoke_status.log; then + fail "of.sh status -- looks like a real crash, see /tmp/smoke_status.log" + elif grep -q "openFrameworks" /tmp/smoke_status.log; then + pass "of.sh status ran to completion (reported libs/PG state, no crash)" + else + fail "of.sh status -- unexpected output, see /tmp/smoke_status.log" + fi + + # pick any one real downloaded lib dir and confirm it's correctly seen as + # NOT git-tracked, so cleanLibraries' nuclear/platform modes would still + # be able to remove it (this is the exact protection added for ofxKinect + # etc. — confirming it isn't overly broad) + SAMPLE_LIB=$(find "$WT_DIR/libs" -mindepth 1 -maxdepth 1 -type d ! -name openFrameworks ! -name openFrameworksCompiled ! -name download ! -name scripts 2>/dev/null | head -1) + if [[ -n "$SAMPLE_LIB" ]]; then + ( + cd "$WT_DIR" || exit 1 + OF_DIR="$WT_DIR" bash -c ' + source scripts/ui.sh + source <(sed -n "/^pathIsGitTracked/,/^}/p" scripts/of.sh) + if pathIsGitTracked "'"$SAMPLE_LIB"'"; then + echo "TRACKED" + else + echo "NOT_TRACKED" + fi + ' + ) > /tmp/smoke_tracked.log 2>&1 + if grep -q NOT_TRACKED /tmp/smoke_tracked.log; then + pass "downloaded lib ($SAMPLE_LIB) correctly NOT protected by git-tracked check" + else + fail "downloaded lib ($SAMPLE_LIB) -- $(cat /tmp/smoke_tracked.log)" + fi + else + fail "no downloaded lib dir found under $WT_DIR/libs to test" + fi + + if [[ "$KEEP" -eq 1 ]]; then + echo + echo " --keep passed: worktree left at $WT_DIR for inspection" + echo " remove later with: git worktree remove --force $WT_DIR" + else + git worktree remove --force "$WT_DIR" >/dev/null 2>&1 \ + && pass "cleaned up worktree" \ + || fail "could not remove worktree at $WT_DIR — remove manually" + fi +fi + +# --------------------------------------------------------------------------- +section "Summary" +# --------------------------------------------------------------------------- +echo " $PASS passed, $FAIL failed" +[[ "$FAIL" -eq 0 ]]