# Moss pole configurator: a Phoenix LiveView page that renders mosspole.scad
# and mosspole-pipe.scad in the browser, with OpenSCAD compiled to
# WebAssembly. Build from the repository root:
#
#   docker build -f web/Dockerfile -t mosspole-web .
#   docker run --rm -p 4000:4000 mosspole-web

ARG ALPINE=3.22.7
ARG ELIXIR_IMAGE=hexpm/elixir:1.19.7-erlang-28.4.3-alpine-${ALPINE}

# OpenSCAD for the browser, built with Emscripten on OpenSCAD's own base
# image, which holds its libraries already compiled to WebAssembly. The image
# exists for amd64 only; Docker emulates it elsewhere, and the result runs on
# any platform.
#
# Two builds (ADR 63). The one browsers get is built with WebAssembly's own
# exceptions, several times faster. OpenSCAD's own build emulates C++
# exceptions in JavaScript; it stays as the fallback for browsers without
# them (Chrome before 95, Firefox before 100, Safari before 15.2), which the
# page's worker tells apart.
FROM --platform=linux/amd64 openscad/wasm-base@sha256:f73d33d5f2fd4c7ae4d3aaacb1e2e2deb193b878b38bb80c8235c933ac340c66 AS wasm-base
ARG OPENSCAD_REF=21780e0987e72f6ea9ceaa5a8e2d41f58fe66b96
WORKDIR /src
RUN git init -q openscad && cd openscad \
 && git remote add origin https://github.com/openscad/openscad.git \
 && git fetch -q --depth 1 origin ${OPENSCAD_REF} && git checkout -q FETCH_HEAD \
 && git submodule update -q --init --depth 1 \
      submodules/manifold submodules/Clipper2 submodules/sanitizers-cmake

# The fallback: OpenSCAD as it builds itself for the browser.
FROM wasm-base AS wasm-compat
# Each compile job can take well over a gigabyte with CGAL; set this to the
# number of CPUs where Docker has about 2 GB of memory for each.
ARG BUILD_JOBS=4
RUN emcmake cmake -S openscad -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DWASM_BUILD_TYPE=web -DENABLE_TESTS=OFF -DEXPERIMENTAL=OFF -DUSE_CCACHE=OFF \
 && cmake --build build --parallel ${BUILD_JOBS}

# The libraries of the base image that throw C++ exceptions or use setjmp,
# built again with WebAssembly's own exceptions (web/wasm/libs.sh).
FROM wasm-base AS wasm-native-libs
ENV CFLAGS="-fwasm-exceptions -O3" CXXFLAGS="-fwasm-exceptions -O3" LDFLAGS="-fwasm-exceptions -O3"
COPY web/wasm/libs.sh /wasm/libs.sh
RUN FLAGS="-fwasm-exceptions" sh /wasm/libs.sh

# The build browsers get: OpenSCAD with WebAssembly's own exceptions, from a
# copy of its source whose CMakeLists.txt asks for them.
FROM wasm-native-libs AS wasm-native
ARG BUILD_JOBS=4
WORKDIR /src
RUN cp -r openscad openscad-native \
 && sed -i 's/-fexceptions/-fwasm-exceptions/g; /-sDISABLE_EXCEPTION_CATCHING=0/d' openscad-native/CMakeLists.txt \
 && grep -q -- -fwasm-exceptions openscad-native/CMakeLists.txt \
 && emcmake cmake -S openscad-native -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DWASM_BUILD_TYPE=web -DENABLE_TESTS=OFF -DEXPERIMENTAL=OFF -DUSE_CCACHE=OFF \
 && cmake --build build --parallel ${BUILD_JOBS}

# The same libraries built again for threads, with oneTBB (ADR 64).
FROM wasm-base AS wasm-threads-libs
ENV CFLAGS="-fwasm-exceptions -pthread -O3" CXXFLAGS="-fwasm-exceptions -pthread -O3" \
    LDFLAGS="-fwasm-exceptions -pthread -O3"
COPY web/wasm/libs.sh /wasm/libs.sh
RUN FLAGS="-fwasm-exceptions -pthread" THREADS=1 sh /wasm/libs.sh

# The threaded build, for browsers that give the page shared memory: as the
# native one, with OpenSCAD's and Manifold's parallel code on oneTBB (as on
# the desktop, once the line that turns it off for the browser is gone), and
# a pool of threads, as many as the worker asks for (Module.threads, one per
# core or fewer) and one for main(), started with OpenSCAD. The patch lets
# the worker cap oneTBB to as many (OPENSCAD_THREADS), and the worker may
# give it smaller shared memory than the 4 GB it allows (ADR 66).
# main() runs on a thread of its own (PROXY_TO_PTHREAD): a worker busy
# running code cannot start the threads oneTBB asks for, so the one that
# loads OpenSCAD has to stay free (ADR 64).
FROM wasm-threads-libs AS wasm-threads
ARG BUILD_JOBS=4
WORKDIR /src
COPY web/wasm/openscad-threads.patch /wasm/openscad-threads.patch
RUN cp -r openscad openscad-threads \
 && git -C openscad-threads apply /wasm/openscad-threads.patch \
 && sed -i 's/-fexceptions/-fwasm-exceptions/g; /-sDISABLE_EXCEPTION_CATCHING=0/d; /set(MANIFOLD_PAR OFF CACHE BOOL "" FORCE)/d' \
      openscad-threads/CMakeLists.txt \
 && ! grep -q 'MANIFOLD_PAR OFF' openscad-threads/CMakeLists.txt \
 && emcmake cmake -S openscad-threads -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DWASM_BUILD_TYPE=web -DENABLE_TESTS=OFF -DEXPERIMENTAL=OFF -DUSE_CCACHE=OFF \
      -DCMAKE_EXE_LINKER_FLAGS="-pthread -sPROXY_TO_PTHREAD -sPTHREAD_POOL_SIZE=Module.threads+1 -sMALLOC=mimalloc" \
 && cmake --build build --parallel ${BUILD_JOBS}

# The three builds: the native one in /openscad/, the threaded one in
# /openscad/threads/ and the fallback in /openscad/compat/.
FROM wasm-base AS wasm
ARG OPENSCAD_REF=21780e0987e72f6ea9ceaa5a8e2d41f58fe66b96
COPY --from=wasm-native /src/build/openscad.js /src/build/openscad.wasm /out/openscad/
COPY --from=wasm-threads /src/build/openscad.js /src/build/openscad.wasm /out/openscad/threads/
COPY --from=wasm-compat /src/build/openscad.js /src/build/openscad.wasm /out/openscad/compat/
# The page sends openscad.wasm to every visitor, so it offers the source of
# what is in it from the same place, for the GPL and LGPL (see the licences
# page and ADR 13): OpenSCAD as built, without the tests and the parts of
# Manifold its build leaves out, and CGAL's headers from the base image,
# which are its source. The native build's change to it is the sed above,
# in this Dockerfile, which the page offers too.
RUN mkdir -p /out/source \
 && tar --exclude=.git --exclude=openscad/tests \
      --exclude=openscad/submodules/manifold/samples --exclude=openscad/submodules/manifold/test \
      --exclude=openscad/submodules/manifold/extras --exclude=openscad/submodules/manifold/docs \
      -cJf /out/source/openscad-${OPENSCAD_REF}.tar.xz openscad \
 && tar -cJf /out/source/cgal-6.0.1-headers.tar.xz -C /emsdk/upstream/emscripten/cache/sysroot/include CGAL

# Just the three builds, for running the app outside Docker:
#   docker build -f web/Dockerfile --target openscad-wasm --output web/priv/static/openscad .
FROM scratch AS openscad-wasm
COPY --from=wasm /out/openscad/ /

# The sources of the other copyleft libraries in openscad.wasm, and the
# recipe of the base image (openscad/openscad-wasm, as of the image's build),
# checked against their checksums.
FROM alpine:${ALPINE} AS sources
WORKDIR /out
RUN apk add --no-cache curl \
 && curl -fsSLO https://gmplib.org/download/gmp/gmp-6.3.0.tar.xz \
 && curl -fsSLO https://www.mpfr.org/mpfr-4.2.1/mpfr-4.2.1.tar.xz \
 && curl -fsSL -o glib-17a1183996f85be8814f4cccc2400c93a2e53cbf.tar.gz \
      https://github.com/kleisauke/glib/archive/17a1183996f85be8814f4cccc2400c93a2e53cbf.tar.gz \
 && curl -fsSL -o openscad-wasm-501694addd7a7c9bd307b38d8591a4faad5381e8.tar.gz \
      https://github.com/openscad/openscad-wasm/archive/501694addd7a7c9bd307b38d8591a4faad5381e8.tar.gz \
 && printf '%s\n' \
      "a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898  gmp-6.3.0.tar.xz" \
      "277807353a6726978996945af13e52829e3abd7a9a5b7fb2793894e18f1fcbb2  mpfr-4.2.1.tar.xz" \
      "23bcb242533deef021a10b6a313fc4f977fe9f951295804a60149f8cad55c638  glib-17a1183996f85be8814f4cccc2400c93a2e53cbf.tar.gz" \
      "de15ea60d9caa05c76be3d5e8557ce823b75fcc905e06006811c0483526da088  openscad-wasm-501694addd7a7c9bd307b38d8591a4faad5381e8.tar.gz" \
    | sha256sum -c -

# three.js for the 3D view, served by the page itself so the browser loads
# nothing from elsewhere (ADR 29): the npm package, checked, and only the
# files the page imports, with the licence. The add-ons import three.js by
# its path, since the page has no import map (ADR 63).
FROM alpine:${ALPINE} AS three
ARG THREE_VERSION=0.170.0
WORKDIR /out
RUN apk add --no-cache curl \
 && curl -fsSLO https://registry.npmjs.org/three/-/three-${THREE_VERSION}.tgz \
 && echo "4a608a355dcaba72e0e5383cdc814303f5b6060b43c238cdf6932dceb699238d  three-${THREE_VERSION}.tgz" | sha256sum -c - \
 && tar xzf three-${THREE_VERSION}.tgz package/LICENSE package/build/three.module.js \
      package/examples/jsm/controls/OrbitControls.js package/examples/jsm/loaders/STLLoader.js \
      package/examples/jsm/renderers/CSS2DRenderer.js \
 && mv package three && rm three-${THREE_VERSION}.tgz \
 && sed -i "s|from 'three';|from '/three/build/three.module.js';|" three/examples/jsm/*/*.js \
 && ! grep -rl "from 'three'" three/examples

# Only those files, for running without Docker:
#   docker build -f web/Dockerfile --target three-files --output web/priv/static/three .
FROM scratch AS three-files
COPY --from=three /out/three/ /

# The preview cards, one for each name in web/card/domains.txt, from the
# committed pictures of the model (web/card/make_card.py, ADR 28): the PNG,
# the MP4 with the moss pole turning, made with ffmpeg, and the animated
# WebP of random moss poles (ADR 56).
FROM alpine:${ALPINE} AS cards
RUN apk add --no-cache python3 py3-pillow ffmpeg
COPY web/card /src/web/card
COPY fonts /src/fonts
RUN python3 /src/web/card/make_card.py cards /out

# The tests, as a stage of their own: the deploy job builds it before the
# release (--target test), so a failing test stops the deploy. Nothing of it
# goes into the image.
FROM ${ELIXIR_IMAGE} AS test
ENV MIX_ENV=test MOSSPOLE_MODELS=/model
WORKDIR /app
RUN mix local.hex --force && mix local.rebar --force
COPY web/mix.exs web/mix.lock ./
RUN mix deps.get && mix deps.compile
COPY mosspole.scad mosspole.json /model/
COPY fonts /model/fonts
COPY web/config config
COPY web/lib lib
COPY web/priv priv
COPY web/test test
RUN mix compile --warnings-as-errors && mix test

# The Phoenix release, with OpenSCAD among its static files.
FROM ${ELIXIR_IMAGE} AS app
ENV MIX_ENV=prod
WORKDIR /app
RUN mix local.hex --force && mix local.rebar --force
COPY web/mix.exs web/mix.lock ./
RUN mix deps.get --only prod
COPY web/config config
RUN mix deps.compile
COPY web/lib lib
COPY web/priv priv
COPY --from=wasm /out/openscad/ priv/static/openscad/
COPY --from=wasm /out/source/ priv/static/source/
COPY --from=sources /out/ priv/static/source/
COPY --from=cards /out/ priv/static/cards/
COPY --from=three /out/three/ priv/static/three/
COPY web/Dockerfile priv/static/source/Dockerfile
COPY web/wasm/openscad-threads.patch web/wasm/libs.sh priv/static/source/
RUN gzip -9 -k $(find priv/static/openscad -type f) priv/static/*.js priv/static/*.css $(find priv/static/three -name '*.js') \
 && (cd priv/static/source && sha256sum *.tar.* Dockerfile openscad-threads.patch libs.sh > SHA256SUMS) \
 && mix compile && mix release

FROM alpine:${ALPINE}
RUN apk add --no-cache libstdc++ libgcc ncurses-libs openssl \
 && adduser -D -h /app app
COPY --from=app --chown=app /app/_build/prod/rel/mosspole /app
COPY mosspole.scad mosspole.json /app/model/
COPY fonts /app/model/fonts
USER app
WORKDIR /app
# The commit the image was built from, for the build_info figure.
ARG GIT_COMMIT=unknown
ENV PHX_SERVER=true PORT=4000 MOSSPOLE_MODELS=/app/model MOSSPOLE_COMMIT=${GIT_COMMIT}
EXPOSE 4000
CMD ["/app/bin/mosspole", "start"]
