xvrh / puppeteer-dart

A Dart library to automate the Chrome browser over the DevTools Protocol. This is a port of the Puppeteer API
BSD 3-Clause "New" or "Revised" License
230 stars 58 forks source link

Trying to run Puppeteer on Oracle Linux 9 aarch64 #275

Closed TiffApps closed 9 months ago

TiffApps commented 9 months ago

Hi! I know that linux_arm64 isn't supported, because there is no official arm64-compatible Chromium release, but there are some on the EPEL repos. Knowing that, it should be possible to run it right?

For context, I'm trying to make a Dart image generator by loading an HTML template, filling all the infos inside, then throwing this to Puppeteer in headless mode so it can take a screenshot. It's running on an Oracle Cloud Instance (OCI) Ampere1 Flex (ARM64), with Oracle Linux 9.2 as image. Dart is version 3.2.2 (latest stable).

Here is what I tried with my Dockerfile:

# Stage 1: Build the Dart application
FROM dart:stable AS dartbuild
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server

# Stage 2: Set up Oracle Linux environment for Chromium
FROM oraclelinux:9 AS oraclelinux
RUN dnf install -y dnf-plugins-core \
    && dnf config-manager --add-repo=http://yum.oracle.com/repo/OracleLinux/OL9/developer/EPEL/aarch64 \
    && dnf update -y \
    && dnf install -y chromium \
    && dnf clean all

# Stage 3: Set up the final image with Debian for SQLite
FROM debian:bullseye-slim

# Copy Chromium and its dependencies
COPY --from=oraclelinux /usr/bin/chromium-browser /usr/bin/
COPY --from=oraclelinux /usr/lib64/chromium-browser/ /usr/lib64/chromium-browser/
COPY --from=oraclelinux /etc/chromium/ /etc/chromium/
COPY --from=oraclelinux /lib64 /lib64
COPY --from=oraclelinux /usr/lib64 /usr/lib64

# Correct the chromium-browser script
RUN sed -i '1s|#!/usr/bin/bash|#!/bin/bash|' /usr/bin/chromium-browser

# Install SQLite and other necessary tools
RUN apt-get update && apt-get install -y sqlite3 libsqlite3-dev unzip && rm -rf /var/lib/apt/lists/*

# Set the working directory in the container
WORKDIR /app

# Copy the AOT-compiled server and runtime from the Dart build stage
COPY --from=dartbuild /runtime/ /
COPY --from=dartbuild /app/bin/server /app/bin/
COPY --from=dartbuild /app/assets /app/assets/

# Start server and expose the necessary port
EXPOSE 8080
CMD ["/app/bin/server"]

As you can see, sqlite3 is a requirement for my project as well and it's not available on OL9's EPEL repo, so have to do like this for now. (I'm not very familiar with Docker, so this file might not appear optimal btw...)

However when I run my code, I get an Exception: Not able to connect to Chrome DevTools. I tried docker exec -it <container> /bin/bash to peek inside and run it manually but I got this:

root@container:/app# /usr/lib64/chromium-browser/chromium-browser
/usr/lib64/chromium-browser/chromium-browser: error while loading shared libraries: libatomic.so.1: cannot open shared object file: No such file or directory

It took me 2 full days to get to this point, and as motivated as I am, I'm getting really worn out by the constant errors and hours of research that follow each of them, just to get new ones.

Hence my question: Is there any image to run Dart Puppeteer in a Docker with an arm64 architecture? Or any alternative, I just want to take a screenshot of my headless html page...

(Things that I can't do without for other reasons: arm64 architecture, sqlite3)

Best regards!

TiffApps commented 9 months ago

Okay nvm, I took a break, then started from scratch again and got it to work first try somehow...

Here is my Dockerfile and my code, since it seems many people struggled with that from what I found on Google:

FROM dart:stable AS dartbuild
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server

FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y \
    sqlite3 \
    libsqlite3-dev \
    chromium \
    wget \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatspi2.0-0 \
    libdrm2 \
    libgbm1 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    xdg-utils \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=dartbuild /runtime/ /
COPY --from=dartbuild /app/bin/server /app/bin/
COPY --from=dartbuild /app/assets /app/assets/

EXPOSE 8080
CMD ["/app/bin/server"]

Dart code:

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:puppeteer/puppeteer.dart';

Future<String?> imageToBase64(String imagePath) async {
  final bytes = await File(imagePath).readAsBytes();
  final base64String = base64Encode(bytes);
  return 'data:image/png;base64,$base64String';
}

Future<String> generateRecordImageFromTemplate(
    Record record, Map<String, Object?> infos) async {
  Logger.root.level = Level.SEVERE;
  final logger = Logger('HTML Generation');
  final basePath = Platform.environment['BASE_PATH'] ?? '';
  String updatedHtml = '';
  try {
    // Load the HTML template and fill it with the record data
    final recordPath = 'assets/templates/record';
    final templatePath = '$basePath/$recordPath/record_template.html';
    final htmlTemplate = await File(templatePath).readAsString();
    final backgroundImage =
        (await imageToBase64('$basePath/${infos['image'].toString()}'))!;
    // ...
    updatedHtml = htmlTemplate
        .replaceAll('{{top}}', infos['top'].toString())
        .replaceAll('{{left}}', infos['left'].toString())
        .replaceAll('{{size}}', infos['size'].toString())
        .replaceAll('{{mirror}}', infos['mirror'].toString())
        .replaceAll('{{index}}', infos['index'].toString())
        .replaceAll('{{rotate}}', infos['rotation'].toString())
        // Record infos
        .replaceAll(
            '{{value}}', NumberFormat('#,##0', 'en_US').format(record.value))
        .replaceAll('{{type}}', record.type)
        .replaceAll('{{background}}', backgroundImage)
        .replaceAll('{{fontSize}}', record.player.length <= 13 ? '150' : '100');
  } catch (e) {
    logger.severe('Failed to load and fill the HTML file: $e');
    rethrow;
  }

  late Browser browser;
  late Uint8List bytes;

  try {
    // Open the HTML page and take the screenshot
    browser = await puppeteer
        .launch(executablePath: '/usr/bin/chromium', headless: true, args: [
      '--no-sandbox',
      '--disable-gpu',
    ]);
    final page = await browser.newPage();
    await page.setContent(updatedHtml, wait: Until.networkIdle);
    bytes = await page.screenshot(omitBackground: true);
  } catch (e) {
    logger.severe('Failed to open the HTML page and take the screenshot: $e');
    rethrow;
  }

  late String imagePath;

  try {
    // Ensure the directory exists then save the screenshot
    final dir = '$basePath/shared/records/';
    final name = '${record.id}-${record.type}-record.png';
    imagePath = path.join(dir, name);
    await Directory(dir).create(recursive: true);
    await File(imagePath).writeAsBytes(bytes, mode: FileMode.writeOnly);
    await browser.close();

    Logger.root.level = Level.INFO;
  } catch (e) {
    logger.severe('Failed to save the screenshot and close the browser: $e');
    rethrow;
  }

  return imagePath;
}

I hope it will help someone else!