scripts/build-deb.sh
#!/usr/bin/env bash

# Fail on error
set -e

# Echo commands
set -x

# Check if dpkg-deb is available
if ! command -v dpkg-deb >/dev/null 2>&1; then
  echo "dpkg-deb could not be found"
  echo "Please install dpkg-deb before running this script"
  exit 1
fi

# Service variables
# These represent the required directories and files for the service
SERVICE_NAME="medplum"
TMP_DIR="medplum-deb"
LIB_DIR="$TMP_DIR/usr/lib/$SERVICE_NAME"
ETC_DIR="$TMP_DIR/etc/$SERVICE_NAME"
VAR_DIR="$TMP_DIR/var/lib/$SERVICE_NAME"
SYSTEM_DIR="$TMP_DIR/lib/systemd/system"
DEBIAN_DIR="$TMP_DIR/DEBIAN"
NGINX_SITES_AVAILABLE_DIR="$TMP_DIR/etc/nginx/sites-available"

# Placeholder values for app build
# These are same placeholder values used in `.github/build.yml`
MEDPLUM_BASE_URL="__MEDPLUM_BASE_URL__"
MEDPLUM_CLIENT_ID="__MEDPLUM_CLIENT_ID__"
MEDPLUM_REGISTER_ENABLED="__MEDPLUM_REGISTER_ENABLED__"
MEDPLUM_AWS_TEXTRACT_ENABLED="__MEDPLUM_AWS_TEXTRACT_ENABLED__"
GOOGLE_CLIENT_ID="__GOOGLE_CLIENT_ID__"
RECAPTCHA_SITE_KEY="__RECAPTCHA_SITE_KEY__"

# Get version
VERSION=$(node -p "require('./package.json').version")
echo "Building version $VERSION"

# Clear previous builds
rm -rf "$TMP_DIR"
rm -rf "$SERVICE_NAME-$VERSION.deb"

# Copy package files
PACKAGES=("app" "ccda" "core" "definitions" "fhirtypes" "fhir-router" "react" "react-hooks" "server")
for package in ${PACKAGES[@]}; do
  echo "Copy $package"
  mkdir -p "$LIB_DIR/packages/$package"
  cp "packages/$package/package.json" "$LIB_DIR/packages/$package"
  cp -r "packages/$package/dist" "$LIB_DIR/packages/$package"
done

# Copy root package.json
cp package.json "$LIB_DIR"
cp package-lock.json "$LIB_DIR"

# Create the server config
mkdir -p "$ETC_DIR"
cp packages/server/medplum.config.json "$ETC_DIR"
sed -i "s|file:./binary/|file:/var/lib/$SERVICE_NAME/binary/|g" "$ETC_DIR/medplum.config.json"

# Create the data directory
mkdir -p "$VAR_DIR/binary"
echo "Medplum data files" > "$VAR_DIR/README.txt"

# Move into the working directory
pushd "$LIB_DIR"

# Install dependencies
npm ci --omit=dev --omit=optional --omit=peer

# Move back to the original directory
popd

# Create the systemd service definition
mkdir -p "$SYSTEM_DIR"

# Create the service file
cat > "$SYSTEM_DIR/$SERVICE_NAME.service" <<EOF
[Unit]
Description=Medplum
After=network.target

[Service]
ExecStart=/usr/bin/node /usr/lib/$SERVICE_NAME/packages/server/dist/index.js "file:/etc/$SERVICE_NAME/medplum.config.json"
Restart=always
User=$SERVICE_NAME
Group=$SERVICE_NAME
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/usr/lib/$SERVICE_NAME

[Install]
WantedBy=multi-user.target
EOF

# Create the nginx sites-available directory
mkdir -p "$NGINX_SITES_AVAILABLE_DIR"

# Create the app site configuration
cat > "$NGINX_SITES_AVAILABLE_DIR/medplum-app" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    # Redirect HTTP to HTTPS
    location / {
        return 301 https://\$server_name\$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # React app built files location
    root /usr/lib/medplum/packages/app/dist;
    index index.html;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Main location block
    location / {
        try_files \$uri \$uri/ /index.html;
    }

    # HTML files should not be cached (for SPA routing)
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        expires 0;
    }

    # All other files can be cached (they have content-based hashes)
    location ~* \.(?!html$) {
        expires 1y;
        add_header Cache-Control "public, no-transform";
    }
}
EOF

# Create the server site configuration
cat > "$NGINX_SITES_AVAILABLE_DIR/medplum-server" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    # Redirect HTTP to HTTPS
    location / {
        return 301 https://\$server_name\$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://localhost:8103;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host \$host;
        proxy_cache_bypass \$http_upgrade;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOF

# Create Debian files
mkdir -p "$DEBIAN_DIR"

# Create the Debian template variables
cat > "$DEBIAN_DIR/templates" <<EOF
Template: medplum/server_hostname
Type: string
Default: api.example.com
Description: API Server Hostname
 Enter the hostname where the Medplum API server will be accessible.

Template: medplum/app_hostname
Type: string
Default: app.example.com
Description: Web Application Hostname
 Enter the hostname where the Medplum web application will be accessible.

Template: medplum/db_host
Type: string
Default: localhost
Description: Database Host
 Enter the PostgreSQL server hostname.
 Leave as localhost if the database is on this machine.

Template: medplum/db_password
Type: password
Description: Database Password
 Choose a password for the Medplum database user.
 This will be used to create the initial database user.
EOF

# Create the Debian config script
# This file will be executed by debconf to ask the user for configuration values
cat > "$DEBIAN_DIR/config" <<EOF
#!/bin/sh
set -e

# Source debconf library
. /usr/share/debconf/confmodule

# Ask the questions
db_input high medplum/server_hostname || true
db_input high medplum/app_hostname || true
db_input medium medplum/db_host || true
if db_get medplum/db_host && [ "\$RET" != "localhost" ]; then
    db_input high medplum/db_password || true
fi
db_go || true
EOF

# Create the Debian control file
# "Depends" is a list of packages that must be installed
# "Recommends" is a list of packages that are recommended, and will be installed by default
# "Suggests" is a list of packages that are suggested, but not installed by default
cat > "$DEBIAN_DIR/control" <<EOF
Package: $SERVICE_NAME
Version: $VERSION
Section: base
Priority: optional
Architecture: all
Depends: debconf, nodejs
Recommends: nginx, postgresql-16, redis-server
Suggests: certbot
Maintainer: Medplum <hello@medplum.com>
Description: Medplum Server
EOF

# Create the Debian post-install script
cat > "$DEBIAN_DIR/postinst" <<EOF
#!/bin/sh
set -e

# Source debconf library
. /usr/share/debconf/confmodule

# Get the configured values
db_get medplum/server_hostname
SERVER_HOSTNAME="\$RET"
db_get medplum/app_hostname
APP_HOSTNAME="\$RET"
db_get medplum/db_host
DB_HOST="\$RET"
db_get medplum/db_password
DB_PASSWORD="\$RET"

# Update the app config
# Recursively apply to all text files in the app dist directory
find "/usr/lib/medplum/packages/app/dist" -type f -exec sed -i \
  -e "s|__MEDPLUM_BASE_URL__|https://\${SERVER_HOSTNAME}/|g" \
  -e "s|__MEDPLUM_CLIENT_ID__|\${MEDPLUM_CLIENT_ID}|g" \
  -e "s|__GOOGLE_CLIENT_ID__|\${GOOGLE_CLIENT_ID}|g" \
  -e "s|__RECAPTCHA_SITE_KEY__|\${RECAPTCHA_SITE_KEY}|g" \
  -e "s|__MEDPLUM_REGISTER_ENABLED__|\${MEDPLUM_REGISTER_ENABLED}|g" \
  {} \;

# Update the server config
if [ -f /etc/medplum/medplum.config.json ]; then
    # Update baseUrl
    sed -i "s|\"baseUrl\":[[:space:]]*\"[^\"]*\"|\"baseUrl\": \"https://\$SERVER_HOSTNAME/\"|" /etc/medplum/medplum.config.json

    # Update appBaseUrl
    sed -i "s|\"appBaseUrl\":[[:space:]]*\"[^\"]*\"|\"appBaseUrl\": \"https://\$APP_HOSTNAME/\"|" /etc/medplum/medplum.config.json
fi

# Update nginx configurations if they exist
if [ -f /etc/nginx/sites-available/medplum-server ]; then
    sed -i "s|api\.example\.com|\$SERVER_HOSTNAME|g" /etc/nginx/sites-available/medplum-server
    sed -i "s|app\.example\.com|\$APP_HOSTNAME|g" /etc/nginx/sites-available/medplum-server
fi

if [ -f /etc/nginx/sites-available/medplum-app ]; then
    sed -i "s|api\.example\.com|\$SERVER_HOSTNAME|g" /etc/nginx/sites-available/medplum-app
    sed -i "s|app\.example\.com|\$APP_HOSTNAME|g" /etc/nginx/sites-available/medplum-app
fi

# Create the Medplum user
addgroup --system $SERVICE_NAME
adduser --system --ingroup $SERVICE_NAME $SERVICE_NAME
chown $SERVICE_NAME:$SERVICE_NAME "/var/lib/$SERVICE_NAME"

# Start or restart the service
if [ "\$1" = "configure" ]; then
    systemctl daemon-reload
    systemctl enable $SERVICE_NAME.service
    if [ -n "\$2" ]; then
        # Restart the service if this is an upgrade
        systemctl restart $SERVICE_NAME
    else
        # Start the service if this is a fresh install
        systemctl start $SERVICE_NAME
    fi
fi
EOF

# Create the Debian pre-remove script
cat > "$DEBIAN_DIR/prerm" <<EOF
#!/bin/sh
set -e

# Only fully stop/disable on package removal
if [ "\$1" = "remove" ]; then
    systemctl stop $SERVICE_NAME.service
    systemctl disable $SERVICE_NAME.service
fi

# For upgrades, we only need to stop the service temporarily
if [ "\$1" = "upgrade" ]; then
    systemctl stop $SERVICE_NAME.service
fi
EOF

# Set permissions
chmod 755 "$DEBIAN_DIR/config"
chmod 755 "$DEBIAN_DIR/postinst"
chmod 755 "$DEBIAN_DIR/prerm"

# Build the package
# Use the standard Debian package naming convention: <name>_<version>_<architecture>.deb
# We can use architecture "all" because we are not building a binary package
DEB_FILENAME="${SERVICE_NAME}_${VERSION}_all.deb"
dpkg-deb --build --root-owner-group -Zgzip "$TMP_DIR" "$DEB_FILENAME"

# Generate the checksum
CHECKSUM_FILENAME="$DEB_FILENAME.sha256"
sha256sum "$DEB_FILENAME" > "$CHECKSUM_FILENAME"

# Check the checksum
sha256sum --check "$CHECKSUM_FILENAME"

# Cleanup temp files
rm -rf "$TMP_DIR"

# Done
echo "Done"

