# syntax=docker/dockerfile:1.28@sha256:bb22d9815c728170f72750f4e5b0d672e06176142e1d602c7e66c050100b7e5b

ARG PHP_VERSION=8.5
ARG NODE_VERSION=24

FROM php:${PHP_VERSION}-apache AS php-base

RUN set -eux; \
  savedAptMark="$(apt-mark showmanual)"; \
  apt-get update; \
  apt-get install -y --no-install-recommends \
    ca-certificates \
    cron \
    curl \
    libbz2-dev \
    libfreetype6-dev \
    libicu-dev \
    libjpeg62-turbo-dev \
    libpng-dev \
    libzip-dev \
    unzip \
    zlib1g-dev; \
  docker-php-ext-configure gd --with-freetype --with-jpeg; \
  docker-php-ext-install -j"$(nproc)" bz2 gd intl pdo_mysql zip; \
  a2enmod rewrite; \
  apt-mark auto '.*' > /dev/null; \
  apt-mark manual \
    ${savedAptMark} \
    ca-certificates \
    cron \
    curl \
    unzip; \
  find /usr/local -type f \( -perm /0111 -o -name '*.so' \) -exec sh -c 'ldd "$@" 2>/dev/null' sh '{}' + \
    | awk 'NF == 4 && $2 == "=>" { print $3 } NF == 2 && $1 ~ /^\// { print $1 }' \
    | sort -u \
    | while read -r library; do \
        library="$(readlink -e "${library}")"; \
        dpkg-query --search "${library}" 2>/dev/null | grep -v '^diversion ' | head -n1 | cut -d: -f1; \
      done \
    | sort -u \
    | xargs -r apt-mark manual; \
  apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
  apt-get clean; \
  rm -rf /var/lib/apt/lists/* /var/cache/apt/*

FROM php-base AS composer-base

WORKDIR /app

COPY --from=composer:2@sha256:da624c2c705d54e2f5a9bb77a5dc6c7d091711433ebefd7035672e84c2010fee /usr/bin/composer /usr/bin/composer

RUN apt-get update \
  && apt-get install -y --no-install-recommends git \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/* /var/cache/apt/*

FROM composer-base AS php-vendor

COPY composer.json composer.lock ./
COPY src/library ./src/library
COPY src/modules ./src/modules

RUN --mount=type=cache,target=/tmp/composer-cache,id=composer-prod \
  set -eux; \
  find ./src/modules -type d -name tests -prune -exec rm -rf {} +; \
  COMPOSER_CACHE_DIR=/tmp/composer-cache \
  composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction --no-progress

FROM composer-base AS php-dev-vendor
COPY composer.json composer.lock ./
COPY src/library ./src/library
COPY src/modules ./src/modules

RUN --mount=type=cache,target=/tmp/composer-cache,id=composer-dev \
  COMPOSER_CACHE_DIR=/tmp/composer-cache \
  composer install --prefer-dist --optimize-autoloader --no-interaction --no-progress

FROM node:${NODE_VERSION}-bookworm-slim AS frontend-assets

WORKDIR /app

COPY package.json package-lock.json ./
COPY src/themes/default/admin/package.json src/themes/default/admin/package.json
COPY src/themes/default/client/package.json src/themes/default/client/package.json

RUN --mount=type=cache,target=/root/.npm npm ci

COPY tsconfig.json ./
COPY src/themes/default/admin ./src/themes/default/admin
COPY src/themes/default/client ./src/themes/default/client
COPY src/modules ./src/modules
COPY frontend ./frontend

RUN NODE_ENV=production npm run build

FROM php-base AS release-tree

WORKDIR /app

ARG FOSSBILLING_VERSION=0.0.1
ARG FOSSBILLING_VERSION_TRUNCATE=0
ARG SENTRY_DSN=
ARG INSTALL_TRANSLATIONS=true
ARG TRANSLATIONS_URL=https://github.com/FOSSBilling/locale/releases/latest/download/translations.zip
ARG TRANSLATIONS_SHA256=

COPY --from=composer:2@sha256:da624c2c705d54e2f5a9bb77a5dc6c7d091711433ebefd7035672e84c2010fee /usr/bin/composer /usr/bin/composer
COPY composer.json ./composer.json
COPY src ./src
COPY README.md LICENSE ./src/
COPY --from=php-vendor /app/src/vendor ./src/vendor
COPY --from=frontend-assets /app/src/public/assets ./src/public/assets
COPY --from=frontend-assets /app/src/themes/default/admin/assets/build ./src/themes/default/admin/assets/build
COPY --from=frontend-assets /app/src/themes/default/client/assets/build ./src/themes/default/client/assets/build

RUN set -eux; \
  find ./src/modules -type d -name tests -prune -exec rm -rf {} +; \
  mkdir -p ./src/locale; \
  if [ "${INSTALL_TRANSLATIONS}" = "true" ]; then \
    curl -fsSL "${TRANSLATIONS_URL}" -o /tmp/translations.zip; \
    if [ -n "${TRANSLATIONS_SHA256}" ]; then \
      echo "${TRANSLATIONS_SHA256}  /tmp/translations.zip" | sha256sum -c -; \
    fi; \
    unzip -oq /tmp/translations.zip -d ./src/locale; \
    rm /tmp/translations.zip; \
  fi; \
  FOSSBILLING_VERSION="${FOSSBILLING_VERSION}" \
  FOSSBILLING_VERSION_TRUNCATE="${FOSSBILLING_VERSION_TRUNCATE}" \
  SENTRY_DSN="${SENTRY_DSN}" \
  php -r '$version = getenv("FOSSBILLING_VERSION") ?: "0.0.1"; $truncate = (int) (getenv("FOSSBILLING_VERSION_TRUNCATE") ?: 0); if ($truncate > 0) { $version = substr($version, 0, $truncate); } $versionFile = "./src/library/FOSSBilling/Version.php"; $contents = file_get_contents($versionFile); $quote = chr(39); $pattern = "/public const string VERSION = " . $quote . "[^" . $quote . "]+" . $quote . ";/"; $replacement = "public const string VERSION = " . var_export($version, true) . ";"; $contents = preg_replace($pattern, $replacement, $contents, 1, $count); if ($contents === null || $count !== 1) { fwrite(STDERR, "Failed to replace FOSSBilling version.\n"); exit(1); } file_put_contents($versionFile, $contents); $dsn = getenv("SENTRY_DSN"); if ($dsn !== false && $dsn !== "") { $sentryFile = "./src/library/FOSSBilling/SentryHelper.php"; file_put_contents($sentryFile, str_replace("--replace--this--during--release--process--", $dsn, file_get_contents($sentryFile))); }'; \
  php -r '$composer = json_decode(file_get_contents("./composer.json"), true, 512, JSON_THROW_ON_ERROR); $composer["autoload"]["psr-4"]["FOSSBilling\\"] = "library/FOSSBilling/"; $composer["autoload"]["psr-4"]["Box\\Mod\\"] = "modules/"; $composer["autoload"]["classmap"] = ["library/"]; $composer["config"]["vendor-dir"] = "vendor"; file_put_contents("./src/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL);'; \
  composer --no-plugins dump-autoload --working-dir=./src --no-dev --optimize --no-interaction; \
  php -r '$class = "FOSSBilling\\Cache\\CacheFactory"; require "./src/vendor/autoload.php"; if (!class_exists($class)) { fwrite(STDERR, "Release Composer autoloader is missing {$class}.\n"); exit(1); }'; \
  rm ./composer.json ./src/composer.json; \
  chmod -R u=rwX,go=rX ./src

FROM scratch AS release-artifact

COPY --from=release-tree /app/src /

FROM php-base AS runtime

WORKDIR /var/www/html

COPY --from=release-tree --chown=www-data:www-data /app/src/ ./

RUN set -eux; \
  mkdir -p data/cache data/log data/uploads; \
  touch /var/log/cron.log; \
  chown -R www-data:www-data data /var/log/cron.log; \
  echo '*/5 * * * * /usr/local/bin/php /var/www/html/cron.php >> /var/log/cron.log 2>&1' > /tmp/www-data.cron; \
  crontab -u www-data /tmp/www-data.cron; \
  rm /tmp/www-data.cron

CMD ["sh", "-c", "cron & exec apache2-foreground"]

FROM runtime AS test

WORKDIR /workspace

COPY --from=release-tree /app/src ./src
COPY src/modules ./src/modules
COPY --from=php-dev-vendor /app/src/vendor ./src/vendor
COPY composer.json composer.lock phpstan.neon phpunit.xml.dist ./
COPY tests ./tests
COPY tools ./tools

RUN set -eux; \
  php -r '$config = require "./src/config-sample.php"; file_put_contents("./src/config.php", "<?php\nreturn " . var_export($config, true) . ";\n");'; \
  mkdir -p ./src/data/cache ./src/data/log ./src/data/uploads; \
  chown -R www-data:www-data ./src/data ./src/config.php
