Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/source/config_files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ To set the date of the snapshot add a runtime.txt_.
For an example ``install.R`` file, visit our `example install.R file <https://github.com/binder-examples/r/blob/HEAD/install.R>`_.


.. _rstudio.yml:

``rstudio.yml`` - Pin the RStudio Server version
================================================

Repositories with an R environment get RStudio Server installed. By
default, the latest stable release published by Posit is installed at
build time. To pin a specific version instead, add an ``rstudio.yml``:

.. code-block:: yaml

version: 2023.12.1+402
sha256: 2ceeebe5d1d77068b36e85f7cf366cd1409f7642a80261b6bbeb3da945ef0888

Available versions are listed on
`Posit's previous versions page <https://docs.posit.co/previous-versions/rstudio/>`_.
The ``.deb`` package is downloaded from Posit over HTTPS. ``sha256`` is
optional; when given, the download is verified against it.

.. note::

For R versions older than 3.6, which current RStudio releases no longer
support, an older compatible RStudio (2023.12.1) is installed by default
instead of the latest release.


.. _apt.txt:

``apt.txt`` - Install packages with apt-get
Expand Down
5 changes: 4 additions & 1 deletion docs/source/howto/languages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ you need to create a ``runtime.txt`` file with the following format:
This will provide you R of given version (such as 4.1, 3.6, etc), and a CRAN snapshot
to install libraries from on the given date. You can install more R packages from CRAN
by adding a :ref:`install.R<install.R>` file to your repo. RStudio and IRKernel are
installed by default for all R versions.
installed by default for all R versions. The latest stable RStudio Server is
installed at build time (for R older than 3.6, an older compatible RStudio is
installed instead); you can pin a specific version with a
:ref:`rstudio.yml<rstudio.yml>` file.

`packagemanager.posit.co <https://packagemanager.posit.co/client/#/>`_
will be used to provide much faster installations via `binary packages <https://www.rstudio.com/blog/package-manager-v1-1-no-interruptions/>`_.
Expand Down
38 changes: 20 additions & 18 deletions repo2docker/buildpacks/_r_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@
Keeping this in r.py would lead to cyclic imports.
"""

import shlex

from ..semver import parse_version as V
from ._rstudio import rstudio_server_installer


def rstudio_base_scripts(r_version):
def rstudio_base_scripts(r_version, rstudio_config):
"""Base steps to install RStudio and shiny-server."""

# Shiny server (not the package!) seems to be the same version for all R versions
shiny_server_url = "https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-1.5.17.973-amd64.deb"
shiny_proxy_version = "1.1"
shiny_sha256sum = "80f1e48f6c824be7ef9c843bb7911d4981ac7e8a963e0eff823936a8b28476ee"

# RStudio server has different builds based on wether OpenSSL 3 or 1.1 is available in the base
# image. 3 is present Jammy+, 1.1 until then. Instead of hardcoding URLs based on distro, we actually
# check for the dependency itself directly in the code below. You can find these URLs in
# https://posit.co/download/rstudio-server/, toggling between Ubuntu 22 (for openssl3) vs earlier versions (openssl 1.1)
# you may forget about openssl, but openssl never forgets you.

# RStudio server 2023.12.1 seems to be provided for only Ubuntu 20.04 and later
# and it does not work with Ubuntu 18.04(bionic)
rstudio_url = "https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2023.12.1-402-amd64.deb"
rstudio_sha256sum = (
"2ceeebe5d1d77068b36e85f7cf366cd1409f7642a80261b6bbeb3da945ef0888"
)
rstudio_url, rstudio_sha256sum = rstudio_server_installer(r_version, rstudio_config)
# the URL comes from external metadata (downloads.json) or is derived
# from repository-provided rstudio.yml; never splice it in unquoted
rstudio_url = shlex.quote(rstudio_url)
if rstudio_sha256sum:
rstudio_verify = f'echo "{rstudio_sha256sum} /tmp/rstudio.deb" | sha256sum -c -'
else:
# user-pinned version without a hash; trust HTTPS
rstudio_verify = "true"
rsession_proxy_version = "2.2.0"

return [
Expand All @@ -35,15 +35,17 @@ def rstudio_base_scripts(r_version):
# we should have --no-install-recommends on all our apt-get install commands,
# but here it's important because these recommend r-base,
# which will upgrade the installed version of R, undoing our pinned version
#
# RStudio's postinst initializes /var/lib/rstudio-server (session-rpc-key
# etc.) as root; rserver runs as NB_USER and needs to read/write it
rf"""
apt-get update > /dev/null && \
RSTUDIO_URL="{rstudio_url}" && \
RSTUDIO_HASH="{rstudio_sha256sum}" && \
curl --silent --location --fail ${{RSTUDIO_URL}} > /tmp/rstudio.deb && \
curl --silent --location --fail {shiny_server_url} > /tmp/shiny.deb && \
echo "${{RSTUDIO_HASH}} /tmp/rstudio.deb" | sha256sum -c - && \
curl --silent --show-error --location --fail {rstudio_url} > /tmp/rstudio.deb && \
curl --silent --show-error --location --fail {shiny_server_url} > /tmp/shiny.deb && \
{rstudio_verify} && \
echo '{shiny_sha256sum} /tmp/shiny.deb' | sha256sum -c - && \
apt install -y --no-install-recommends /tmp/rstudio.deb /tmp/shiny.deb && \
chown -R ${{NB_USER}}:${{NB_USER}} /var/lib/rstudio-server && \
rm /tmp/*.deb && \
apt-get -qq purge && \
apt-get -qq clean && \
Expand Down
108 changes: 108 additions & 0 deletions repo2docker/buildpacks/_rstudio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Resolution of the RStudio Server version to install.

By default the latest stable release is resolved from Posit's
downloads.json at Dockerfile generation time, so that RStudio keeps up
with current R releases (newer R graphics engines require newer RStudio).

Users can pin a specific version with an `rstudio.yml` in their
repository:

version: 2023.12.1+402
sha256: 2ceeebe5... # optional

Pinned versions are downloaded over HTTPS but only verified against
sha256 when the user provides one.
"""

import os
import re
from functools import lru_cache

import requests
from ruamel.yaml import YAML

from ..semver import parse_version as V

RSTUDIO_DOWNLOADS_URL = "https://www.rstudio.com/wp-content/downloads.json"

# Ubuntu major version of the base image (see Repo2Docker.base_image),
# matched against platform names in downloads.json ("Ubuntu 22/Debian 12").
# The version number is a more stable identity than Posit's series keys,
# whose naming conventions vary across their products.
RSTUDIO_UBUNTU_VERSION = "22"

# Ubuntu series segment of download2.rstudio.org paths, used to construct
# URLs for user-pinned versions without depending on downloads.json
RSTUDIO_UBUNTU_SERIES = "jammy"

# Current RStudio releases require R >= 3.6.0
# https://docs.posit.co/ide/desktop-pro/getting_started/prerequisites.html
RSTUDIO_MIN_R_VERSION = "3.6"

# Last release supporting R >= 3.3, for repos pinning R < 3.6
LEGACY_RSTUDIO_URL = "https://download2.rstudio.org/server/jammy/amd64/rstudio-server-2023.12.1-402-amd64.deb"
LEGACY_RSTUDIO_SHA256 = (
"2ceeebe5d1d77068b36e85f7cf366cd1409f7642a80261b6bbeb3da945ef0888"
)


@lru_cache()
def fetch_latest_rstudio_server():
"""Return (url, sha256) of the latest stable RStudio Server .deb."""
resp = requests.get(RSTUDIO_DOWNLOADS_URL, timeout=30)
resp.raise_for_status()
installers = resp.json()["rstudio"]["open_source"]["stable"]["server"]["installer"]
matches = [
entry
for entry in installers.values()
if re.search(rf"\bUbuntu {RSTUDIO_UBUNTU_VERSION}\b", entry["platform"]["name"])
]
if len(matches) != 1:
names = ", ".join(entry["platform"]["name"] for entry in installers.values())
raise ValueError(
f"could not find exactly one RStudio Server build for "
f"Ubuntu {RSTUDIO_UBUNTU_VERSION} in {RSTUDIO_DOWNLOADS_URL} "
f"(available: {names}); the base image may no longer be within "
f"RStudio's support window"
)
return matches[0]["url"], matches[0]["sha256"]


def load_rstudio_yaml(path):
"""Return the parsed rstudio.yml, or None if it does not exist."""
if not os.path.exists(path):
return None
with open(path) as f:
return YAML().load(f)


def rstudio_server_installer(r_version, rstudio_config):
"""Return (url, sha256) of the RStudio Server .deb to install.

sha256 is None when the user pinned a version without a hash.
"""
if rstudio_config is not None:
# rstudio.yml comes from the repository being built; these values end
# up in root-executed build scripts, so validate them strictly
if "version" not in rstudio_config:
raise ValueError("rstudio.yml must contain a 'version' field")
version = str(rstudio_config["version"]).replace("+", "-")
if not re.fullmatch(r"[0-9A-Za-z.-]+", version):
raise ValueError(
f"invalid RStudio version in rstudio.yml: {rstudio_config['version']!r}"
)
sha256 = rstudio_config.get("sha256")
if sha256 is not None and not re.fullmatch(r"[0-9a-fA-F]{64}", str(sha256)):
raise ValueError("sha256 in rstudio.yml must be a 64-character hex string")
url = f"https://download2.rstudio.org/server/{RSTUDIO_UBUNTU_SERIES}/amd64/rstudio-server-{version}-amd64.deb"
resp = requests.head(url, timeout=30)
if resp.status_code != 200:
raise ValueError(
f"RStudio Server {rstudio_config['version']} specified in rstudio.yml "
f"does not exist: {url} returned {resp.status_code}"
)
return url, sha256
if r_version and V(r_version) < V(RSTUDIO_MIN_R_VERSION):
return LEGACY_RSTUDIO_URL, LEGACY_RSTUDIO_SHA256
return fetch_latest_rstudio_server()
5 changes: 4 additions & 1 deletion repo2docker/buildpacks/conda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ...semver import parse_version as V
from ...utils import is_local_pip_requirement
from .._r_base import rstudio_base_scripts
from .._rstudio import load_rstudio_yaml
from ..base import BaseImage
from .matlab import (
matlab_installation_scripts,
Expand Down Expand Up @@ -442,7 +443,9 @@ def get_env_scripts(self):
raise RuntimeError(
f"RStudio is only available for linux/amd64 ({self.platform})"
)
scripts += rstudio_base_scripts(self.r_version)
scripts += rstudio_base_scripts(
self.r_version, load_rstudio_yaml(self.binder_path("rstudio.yml"))
)
scripts += [
(
"root",
Expand Down
5 changes: 4 additions & 1 deletion repo2docker/buildpacks/r.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ..semver import parse_version as V
from ._r_base import rstudio_base_scripts
from ._rstudio import load_rstudio_yaml
from .python import PythonBuildPack

# Aproximately the first snapshot on RSPM (Posit package manager)
Expand Down Expand Up @@ -304,7 +305,9 @@ def get_build_scripts(self):
),
]

scripts += rstudio_base_scripts(self.r_version)
scripts += rstudio_base_scripts(
self.r_version, load_rstudio_yaml(self.binder_path("rstudio.yml"))
)

scripts += [
(
Expand Down
Loading
Loading