#!/bin/bash # ================== CONFIG ================== SCRIPT_NAME=$(basename "$0") ENV_FILE=".env" WORKDIR="/__w/IfcOpenShell/IfcOpenShell" NAMEPREFIX=ifcopenshell function set_env() { # Load .env file if it exists if [[ -f "$ENV_FILE" ]]; then set -a source "$ENV_FILE" set +a echo "✅ Loaded environment variables from $ENV_FILE" else echo "⚠️ No $ENV_FILE found, proceeding without it." fi } set_env # ================ FUNCTIONS ================= function create() { echo "⭐ Creating image: ifcopenshell-build-env" docker build -f Dockerfile \ --build-arg USER_UID="$(id -u)" --build-arg USER_GID="$(id -g)" \ -t ifcopenshell-build-env:updated . } function update() { # The Dockerfile always builds FROM a clean rockylinux:9 and does # `dnf update -y` as its first step, so re-running create() is enough # to get fresh packages. echo "⚡ Updating image: ifcopenshell-build-env" create } function up() { # Creates the container if it doesn't exist yet (and starts it either # way) - this is the one that needs ready_repo, since a freshly created # container has no submodules/dependency cache in place yet. echo "🚀 Creating/starting stack: ifcopenshell-${UNIQUE_ID}" unique # Update UNIQUE_ID first docker compose up -d "$@" # Container must exist before ready_repo can exec into it. ready_repo # Ensure repo is recursive, and the build repo is in place. } function down() { # Removes the container (and its network) entirely. Named volumes # (ccache) and the bind-mounted repo/build/ survive; up() will recreate # the container from scratch next time. echo "🔥 Removing stack: ifcopenshell-${UNIQUE_ID}" docker compose down "$@" } function stop() { # Stops the existing container without removing it - the container, # its filesystem layer, and its exec history all remain intact. echo "🛑 Stopping stack: ifcopenshell-${UNIQUE_ID}" docker compose stop "$@" } function start() { # Starts a previously-stopped container back up. Does nothing (and # won't create anything) if the container doesn't exist - use up() for # that. echo "▶️ Starting stack: ifcopenshell-${UNIQUE_ID}" docker compose start "$@" } function restart() { echo "🔄 Restarting stack (stop, then start)..." stop start } function recreate() { echo "♻️ Recreating stack (down, then up)..." down up } function logs() { echo "📜 Showing logs..." docker compose logs -f "$@" } function ps() { docker compose ps } function config() { echo "🔍 Validated compose configuration:" docker compose config } function remove() { # Lower-level than down(): removes already-stopped containers without # touching the compose network. Mostly useful after a plain stop(). echo "🗑️ Removing stopped containers: ifcopenshell-${UNIQUE_ID}" docker compose rm "$@" } function unique() { echo "🔧 Making stack name folder specific..." REGEX="^UNIQUE_ID=" if [[ ! -f "$ENV_FILE" ]] || ! grep -qE "$REGEX" "$ENV_FILE"; then echo -e "\nUNIQUE_ID=dummy\n" >> "$ENV_FILE" fi export UNIQUE_ID="$(pwd | sha256sum | cut -c -8)" # `sed -i` takes incompatible syntax between GNU sed (Linux) and BSD sed # (macOS) - `-si` is GNU-only and errors as "illegal option -- s" under # BSD/macOS sed. Avoid -i altogether and do the in-place edit via a temp # file + mv instead, which behaves identically with either sed. local tmp_file tmp_file="$(mktemp "${ENV_FILE}.XXXXXX")" sed "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$ENV_FILE" > "$tmp_file" mv "$tmp_file" "$ENV_FILE" set_env } function ready_repo() { echo "👍 Getting the repo ready to build..." docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c ' set -euo pipefail # Recommended for robustness git submodule update --init --recursive if [[ ! -d "build" ]]; then git clone -b rockylinux9-x64 https://github.com/IfcOpenShell/build-outputs.git build else cd build git pull cd .. fi if [[ ! -d "build/Linux/x86_64/install/boost-1.86.0/" ]]; then cd build uv run ../nix/cache_dependencies.py unpack cd .. fi ' } function build() { echo "☕ Execute the build, go make yourself a cuppa... I'll be a while" local BUILD_TARGET="$1" docker exec -i -w "${WORKDIR}" -e PY_TGT="${PY_TGT}" -e BUILD_TARGET="${BUILD_TARGET}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c ' set -o pipefail CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v ${PY_TGT:+-$PY_TGT} --diskcleanup ${BUILD_TARGET} 2>&1 | tee build.log ' echo "🎒 Pack Dependencies" docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c ' cd build uv run ../nix/cache_dependencies.py pack ' echo "🎁 Package .zip archives" docker exec -i -w "${WORKDIR}" -e GITHUB_SHA="$(git rev-parse HEAD)" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c ' OUTPUT_DIR=${PWD}/output VERSION=v`cat VERSION` mkdir -p ${OUTPUT_DIR} cd ./build/`uname`/*/install/ifcopenshell ls -d python-* | while read py_version; do postfix=`echo ${py_version: -1} | sed s/[0-9]//` numbers=`echo $py_version | grep -oE "[0-9]+\.[0-9]+" | tr -d "."` py_version_major=python-${numbers}$postfix pushd . > /dev/null cd $py_version if [ ! -d ifcopenshell ]; then mkdir ../ifcopenshell_ mv * ../ifcopenshell_ mv ../ifcopenshell_ ifcopenshell fi [ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__ find ifcopenshell -name "*.pyc" -delete zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/* mv *.zip ${OUTPUT_DIR}/ popd > /dev/null done cd bin if compgen -G "./*.zip" > /dev/null; then rm *.zip 2>&1 >/dev/null || true ls | while read exe; do zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe done mv *.zip ${OUTPUT_DIR}/ cd .. ' } function attach() { echo "🔦 Connect to interactive shell" docker exec -it -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" /bin/bash } function try() { # Copies the freshly built wrapper into your actual Blender/Bonsai # installation for manual, in-Blender testing. This is a human-only # convenience: it overwrites files in your live Blender setup, so it's # not something that should run unattended as part of an automated or # AI-driven build/test loop (which should instead copy the wrapper into # the repo's own src/ifcopenshell-python/ifcopenshell/ - see SKILL.md). echo "🚴 Copying build artifacts into your Blender resource folder for testing" if [[ -z "${BLENDER_USER_RESOURCE:-}" ]]; then echo "❌ BLENDER_USER_RESOURCE is not set in .env." echo " Add a line pointing at wherever Blender's user resource folder for" echo " the Bonsai extension actually is on your system, e.g.:" echo " BLENDER_USER_RESOURCE=~/.config/blender/bonsai/" return 1 fi # Normalise: expand a leading ~ (in case it was quoted in .env and so # never went through shell tilde-expansion when set_env sourced it), # then resolve to an absolute, symlink-free path. local resource="${BLENDER_USER_RESOURCE/#\~/$HOME}" resource="$(realpath -m "$resource")" local install_dir="../build/Linux/x86_64/install" local py_dirs=("$install_dir"/python-*) if [[ ${#py_dirs[@]} -gt 1 && -n "${PY_TGT:-}" ]]; then # PY_TGT is compact (py-311); the install dirs are dotted # (python-3.11.8) - reinsert the dot (assumes a single-digit major # version, true for the Python 3.x line) before matching. local py_tgt_digits="${PY_TGT#py-}" local py_tgt_dotted="${py_tgt_digits:0:1}.${py_tgt_digits:1}" local filtered=() d for d in "${py_dirs[@]}"; do [[ "$(basename "$d")" == "python-${py_tgt_dotted}."* ]] && filtered+=("$d") done [[ ${#filtered[@]} -gt 0 ]] && py_dirs=("${filtered[@]}") fi if [[ ${#py_dirs[@]} -ne 1 || ! -d "${py_dirs[0]}" ]]; then echo "❌ Expected exactly one built python-* dir under $install_dir, found ${#py_dirs[@]}." echo " Run 'build' first, or set PY_TGT in .env to disambiguate a multi-version build." return 1 fi local py_minor py_minor="$(basename "${py_dirs[0]}" | grep -oE '[0-9]+\.[0-9]+')" local wrapper_dir="${py_dirs[0]}/lib/python${py_minor}/site-packages/ifcopenshell" if [[ ! -f "$wrapper_dir/ifcopenshell_wrapper.py" ]]; then echo "❌ Built wrapper not found at $wrapper_dir - run 'build' first." return 1 fi local target="$resource/extensions/.local/lib/python${py_minor}/site-packages/ifcopenshell" mkdir -p "$target" cp "$wrapper_dir"/_ifcopenshell_wrapper*.so "$target/" cp "$wrapper_dir"/ifcopenshell_wrapper.py "$target/" echo "✅ Copied wrapper into $target" } function clean() { # Host-side only - doesn't touch the container, image, or ccache volume. echo "💎 Clean the build and output folder up" if [[ -d "../build" ]]; then rm -rf ../build fi if [[ -d "../output" ]]; then rm -rf ../output fi } function help() { cat < Available commands: create Build the rocky9-based image update Rebuild the image fresh, picking up OS package updates up Create the container if it doesn't exist yet, and start it down Remove the container entirely (docker compose down) stop Stop the container without removing it start Start a previously-stopped container restart stop, then start (same container, no recreation) recreate down, then up (fresh container) build Execute the IfcOpenShell build attach Connect to an interactive shell in the container try Copy the built wrapper into your Blender resource folder (human-only - see BLENDER_USER_RESOURCE below, and SKILL.md for the AI/automated-testing equivalent) clean Remove the build and output folders logs Follow container logs ps Show running containers config Validate and show compose config remove Remove stopped containers (docker compose rm) help Show this help Environment variables from .env are automatically loaded, including: PY_TGT Restrict the build to one Python version, e.g. py-311 UNIQUE_ID Recalculated automatically on every 'up', don't set by hand BLENDER_USER_RESOURCE Where 'try' copies the wrapper for manual testing, e.g. ~/.config/blender/bonsai/ EOF } # ================= MAIN ================= case "$1" in create) create ;; update) update ;; up) up "${@:2}" ;; down) down "${@:2}" ;; stop) stop "${@:2}" ;; start) start "${@:2}" ;; restart) restart ;; recreate) recreate ;; build) build "${@:2}" ;; attach) attach ;; try) try ;; clean) clean ;; logs) logs "${@:2}" ;; ps) ps ;; config) config ;; remove) remove ;; help|-h|--help) help ;; "") echo "❌ No command provided." help ;; *) echo "❌ Unknown command: $1" echo "Type './$SCRIPT_NAME help' for available commands." exit 1 ;; esac