mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
92c50ed3b4
Dockerfile (renamed from Dockerfile_init, Dockerfile_update removed): - Run as a non-root `builder` user matching the host UID/GID (passed as --build-arg by create() from id -u/id -g), so build output under the bind mount stays owned by the host user instead of root. - Fix CCACHE_MAXSIZE: `ccache -M 5G` wrote its limit to a config file under /ccache at image-build time, but /ccache is a volume mount point, so that file gets shadowed by the (empty) volume the moment the container actually runs - the cap never took effect. Set CCACHE_MAXSIZE=5G as an image ENV instead. - Dedupe ccache/libffi-devel, add --setopt=install_weak_deps=False --setopt=tsflags=nodocs, add `git lfs install --system`, combine the dnf update+install into one layer. - Drop Dockerfile_update: it built FROM its own previous output, so every `update` call made the image strictly larger forever (Docker layers are append-only, `dnf clean` in a later layer can't shrink an earlier one). `update` now just calls create(), which already runs `dnf update -y` FROM a clean rockylinux:9 every time. compose.yaml: pin platform: linux/amd64 so this doesn't silently run under emulation on an ARM host. ifcos_env: - Split the previously-conflated stop/down into six distinct, Compose-native lifecycle commands: up (create-or-start), down (remove), stop, start, restart (stop+start, same container), recreate (down+up, fresh container). Previously `stop` was aliased to `down`, which silently removed the container instead of pausing it. - Implement try(): copies the built wrapper into a real Blender/Bonsai install for manual testing, reading the target from a new BLENDER_USER_RESOURCE .env variable and auto-detecting the built Python version (disambiguating via PY_TGT for multi-version builds). Deliberately kept human-only - it mutates a live Blender install, so it shouldn't run unattended as part of an automated/AI workflow, which should instead copy the wrapper into the repo's own src/ifcopenshell-python/ifcopenshell/ (documented in SKILL.md). - Fix unique(): the "has .env already got a UNIQUE_ID line" check referenced an unset $FILE instead of $ENV_FILE, so it always evaluated true and appended a fresh "UNIQUE_ID=dummy" line to .env on every single `up`. - Minor: differentiate remove()'s log message from down()'s (no longer identical now that they're distinct operations), tidy help text alignment and a stray double-space typo in clean(). SKILL.md: rewritten as current-state documentation (no more "fixed in this copy" changelog framing) covering the above, plus a migration note for anyone hitting root-owned leftovers from an older image. Verified by actually building the image and driving every new lifecycle command (stop/start/restart keep the same container ID; down+up and recreate produce a new one) and try() (including the quoted-tilde BLENDER_USER_RESOURCE edge case) against the real container. Generated with the assistance of an AI coding tool.
331 lines
11 KiB
Bash
Executable File
331 lines
11 KiB
Bash
Executable File
#!/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 -si "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$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 <<EOF
|
|
Usage: ./$SCRIPT_NAME <command>
|
|
|
|
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
|