diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c3b60c4d..18e7b9b9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,6 +4,8 @@ on: branches: [main, master] pull_request: branches: [main, master] + + jobs: R-CMD-check: runs-on: ubuntu-latest @@ -17,49 +19,78 @@ jobs: with: use-public-rspm: true - uses: r-lib/actions/setup-pandoc@v2 + - name: Setup Python uses: actions/setup-python@v2 with: python-version: '3.9' + - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install flair + - name: Install R dependencies run: | install.packages('remotes') remotes::install_github("davidycliao/flaiR", force = TRUE) shell: Rscript {0} + - uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: rcmdcheck - docker: needs: R-CMD-check runs-on: ubuntu-latest permissions: contents: read packages: write + steps: - - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Log in to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + with: + buildkitd-flags: --debug + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository_owner }}/flair-rstudio + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + type=raw,value=latest,enable={{is_default_branch}} + - name: Build and push RStudio image uses: docker/build-push-action@v4 with: context: . - push: true - tags: | - ghcr.io/${{ github.repository_owner }}/flair-rstudio:latest - labels: | - org.opencontainers.image.title=flaiR-RStudio - org.opencontainers.image.version=0.0.7 + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + DEFAULT_USER=rstudio + secrets: | + PASSWORD=${{ secrets.RSTUDIO_PASSWORD }} cache-from: type=gha cache-to: type=gha,mode=max + provenance: false diff --git a/Dockerfile b/Dockerfile index 6779e479..25250c40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,69 +1,67 @@ FROM rocker/r-ver:latest -# 系統更新和安裝安全性修補 -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y --no-install-recommends \ +# Install system dependencies +RUN apt-get update && apt-get install -y \ python3-minimal \ python3-pip \ python3-venv \ - python3-full \ - libcurl4-openssl-dev \ libssl-dev \ gdebi-core \ wget \ - sudo && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* + sudo \ + curl -# 安裝 RStudio Server -RUN wget --progress=dot:giga https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2023.12.1-402-amd64.deb && \ +# Create rstudio user +ENV USER=rstudio +ENV PASSWORD=rstudio123 +RUN useradd -m $USER && \ + echo "$USER:$PASSWORD" | chpasswd && \ + adduser $USER sudo && \ + echo "$USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Install RStudio Server +RUN wget https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2023.12.1-402-amd64.deb && \ gdebi -n rstudio-server-2023.12.1-402-amd64.deb && \ rm rstudio-server-*.deb -# Python 虛擬環境設置 -RUN python3 -m venv /opt/venv +# Create and configure Python virtual environment +RUN python3 -m venv /opt/venv && \ + chown -R $USER:$USER /opt/venv + ENV PATH="/opt/venv/bin:$PATH" ENV RETICULATE_PYTHON="/opt/venv/bin/python" +# Setup R environment config +RUN mkdir -p /usr/local/lib/R/etc && \ + echo "RETICULATE_PYTHON=/opt/venv/bin/python" >> /usr/local/lib/R/etc/Renviron.site && \ + chown -R $USER:$USER /usr/local/lib/R/etc/Renviron.site && \ + chmod 644 /usr/local/lib/R/etc/Renviron.site -# R env -RUN echo "RETICULATE_PYTHON=/opt/venv/bin/python" >> /usr/local/lib/R/etc/Renviron.site - -# 安裝更新的 Python 包 -RUN /opt/venv/bin/pip install --no-cache-dir --upgrade pip && \ - /opt/venv/bin/pip install --no-cache-dir \ - numpy>=1.26.4 \ - scipy>=1.12.0 \ +# Install Python packages +RUN /opt/venv/bin/pip install --no-cache-dir \ + numpy==1.26.4 \ + scipy==1.12.0 \ transformers \ torch \ flair -# 安裝 R 包 -RUN install2.r --error --deps TRUE \ - reticulate \ - remotes && \ +# Install R packages +RUN R -e "install.packages('reticulate', repos='https://cloud.r-project.org/', dependencies=TRUE)" && \ + R -e "install.packages('remotes', repos='https://cloud.r-project.org/', dependencies=TRUE)" && \ R -e "remotes::install_github('davidycliao/flaiR', dependencies=TRUE)" -# 使用者設置(使用 ARG 而不是 ENV 來處理敏感資訊) -ARG USER=rstudio -ARG PASSWORD=rstudio123 -RUN useradd -m $USER && \ - echo "$USER:$PASSWORD" | chpasswd && \ - adduser $USER sudo +# Set working directory +WORKDIR /home/$USER +RUN chown -R $USER:$USER /home/$USER +# Add healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8787/ || exit 1 -# 設置權限 -RUN mkdir -p /home/$USER && \ - chown -R $USER:$USER /home/$USER && \ - chown -R $USER:$USER /opt/venv && \ - chown -R $USER:$USER /usr/local/lib/R/etc/Renviron.site && \ - chmod 644 /usr/local/lib/R/etc/Renviron.site - -# 清理 -RUN apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - +# Expose port EXPOSE 8787 +# Run service as rstudio user +USER $USER CMD ["/usr/lib/rstudio-server/bin/rserver", "--server-daemonize=0"] + diff --git a/R/zzz.R b/R/zzz.R index bb81acd1..4ebc11c3 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -89,86 +89,113 @@ # } # } .onAttach <- function(...) { - # Check if running in Docker - Sys.unsetenv("RETICULATE_PYTHON") + # Docker 環境檢查及 Python 設置 in_docker <- file.exists("/.dockerenv") - # Determine Python command if (in_docker) { - python_path <- "/opt/venv/bin/python" + # Docker 環境使用固定路徑 + python_path <- "/opt/venv/bin/python3" + Sys.setenv(RETICULATE_PYTHON = python_path) } else { + # 非 Docker 環境 python_cmd <- if (Sys.info()["sysname"] == "Windows") "python" else "python3" python_path <- Sys.which(python_cmd) } - # Check Python path + # 檢查 Python 路徑 if (python_path == "") { - packageStartupMessage("Cannot locate Python executable. Ensure it's installed and in your system's PATH. flaiR functionality requiring Python will not be available.") + packageStartupMessage("Cannot locate Python executable. Ensure Python is installed and in PATH.") return(invisible(NULL)) } - # Check Python version + # 檢查 Python 版本 tryCatch({ - python_version <- system(paste(python_path, "--version"), intern = TRUE) - if (!grepl("Python 3", python_version)) { - packageStartupMessage("Python 3 is required. flaiR functionality requiring Python will not be available.") + python_version <- system2(python_path, "--version", stdout = TRUE, stderr = TRUE) + if (!any(grepl("Python 3", python_version))) { + packageStartupMessage("Python 3 is required.") return(invisible(NULL)) } }, error = function(e) { - packageStartupMessage(paste("Failed to get Python version. Error:", e$message)) + packageStartupMessage(sprintf("Failed to check Python version: %s", e$message)) return(invisible(NULL)) }) - # Version check functions + # 檢查 PyTorch 版本 check_torch_version <- function() { - torch_version_command <- paste(python_path, "-c \"import torch; print(torch.__version__)\"") - result <- system(torch_version_command, intern = TRUE) - if (length(result) == 0 || result[1] == "ERROR" || is.na(result[1])) { - return(list(paste("PyTorch", paste0("\033[31m", "\u2717", "\033[39m"), sep = " "), FALSE)) - } - return(list(paste("PyTorch", paste0("\033[32m", "\u2713", "\033[39m"), result[1], sep = " "), TRUE, result[1])) + tryCatch({ + cmd <- sprintf("%s -c 'import torch; print(torch.__version__)'", python_path) + result <- system(cmd, intern = TRUE) + if (length(result) > 0 && !is.na(result[1])) { + return(list( + status = paste("PyTorch", "\u2713", result[1]), + success = TRUE, + version = result[1] + )) + } + }, error = function(e) NULL) + return(list(status = paste("PyTorch", "\u2717"), success = FALSE)) } + # 檢查 Flair 版本 check_flair_version <- function() { - flair_version_command <- paste(python_path, "-c \"import flair; print(flair.__version__)\"") - result <- system(flair_version_command, intern = TRUE) - if (length(result) == 0 || result[1] == "ERROR" || is.na(result[1])) { - return(list(paste("flair", paste0("\033[31m", "\u2717", "\033[39m"), sep = " "), FALSE)) - } - return(list(paste("flair", paste0("\033[32m", "\u2713", "\033[39m"), result[1], sep = " "), TRUE, result[1])) + tryCatch({ + cmd <- sprintf("%s -c 'import flair; print(flair.__version__)'", python_path) + result <- system(cmd, intern = TRUE) + if (length(result) > 0 && !is.na(result[1])) { + return(list( + status = paste("Flair", "\u2713", result[1]), + success = TRUE, + version = result[1] + )) + } + }, error = function(e) NULL) + return(list(status = paste("Flair", "\u2717"), success = FALSE)) } - flair_version <- check_flair_version() - torch_version <- check_torch_version() + # 執行版本檢查 + flair_check <- check_flair_version() + torch_check <- check_torch_version() - if (isFALSE(flair_version[[2]])) { - packageStartupMessage(sprintf(" Flair %-50s", paste0("is installing from Python"))) + # 如果 Flair 未安裝,嘗試安裝 + if (!flair_check$success) { + packageStartupMessage("Installing Flair and dependencies...") - # Docker-specific installation commands - if (in_docker) { - commands <- c( - paste(python_path, "-m pip install --no-cache-dir numpy==1.26.4"), - paste(python_path, "-m pip install --no-cache-dir torch"), - paste(python_path, "-m pip install --no-cache-dir flair"), - paste(python_path, "-m pip install --no-cache-dir scipy==1.12.0") + # 安裝命令 + install_commands <- if (in_docker) { + c( + sprintf("%s -m pip install --no-cache-dir numpy==1.26.4", python_path), + sprintf("%s -m pip install --no-cache-dir torch", python_path), + sprintf("%s -m pip install --no-cache-dir flair", python_path), + sprintf("%s -m pip install --no-cache-dir scipy==1.12.0", python_path) ) } else { - commands <- c( - paste(python_path, "-m pip install --upgrade pip"), - paste(python_path, "-m pip install torch"), - paste(python_path, "-m pip install flair"), - paste(python_path, "-m pip install scipy==1.12.0") + c( + sprintf("%s -m pip install --upgrade pip", python_path), + sprintf("%s -m pip install torch", python_path), + sprintf("%s -m pip install flair", python_path), + sprintf("%s -m pip install scipy==1.12.0", python_path) ) } - command_statuses <- vapply(commands, system, FUN.VALUE = integer(1)) + # 執行安裝 + install_status <- vapply(install_commands, function(cmd) { + tryCatch({ + system(cmd) + TRUE + }, error = function(e) FALSE) + }, logical(1)) - flair_check_again <- check_flair_version() - if (isFALSE(flair_check_again[[2]])) { - packageStartupMessage("Failed to install Flair. {flaiR} requires Flair NLP. Please ensure Flair NLP is installed in Python manually.") + # 重新檢查安裝 + flair_check <- check_flair_version() + if (!flair_check$success) { + packageStartupMessage("Failed to install Flair. Please install manually.") + return(invisible(NULL)) } - } else { - packageStartupMessage(sprintf("\033[1m\033[34mflaiR\033[39m\033[22m: \033[1m\033[33mAn R Wrapper for Accessing Flair NLP\033[39m\033[22m %-5s", - paste("\033[1m\033[33m", flair_version[[3]], "\033[39m\033[22m", sep = ""))) } + + # 顯示啟動訊息 + packageStartupMessage(sprintf( + "flaiR: An R Wrapper for Accessing Flair NLP %s", + if (flair_check$success) flair_check$version else "" + )) }