From 20c4f48a1c15e70d3df157afae4af9fd4c3bf416 Mon Sep 17 00:00:00 2001 From: Benjamin Collet Date: Wed, 5 Mar 2025 22:23:25 +0000 Subject: [PATCH 1/4] Add feature ocaml-opam --- src/ocaml-opam/devcontainer-feature.json | 29 ++++ src/ocaml-opam/install.sh | 32 +++++ src/ocaml-opam/library_scripts.sh | 173 +++++++++++++++++++++++ test/ocaml-opam/scenarios.json | 16 +++ test/ocaml-opam/test.sh | 9 ++ test/ocaml-opam/test_debian.sh | 12 ++ test/ocaml-opam/test_specific_version.sh | 9 ++ 7 files changed, 280 insertions(+) create mode 100644 src/ocaml-opam/devcontainer-feature.json create mode 100755 src/ocaml-opam/install.sh create mode 100644 src/ocaml-opam/library_scripts.sh create mode 100644 test/ocaml-opam/scenarios.json create mode 100755 test/ocaml-opam/test.sh create mode 100755 test/ocaml-opam/test_debian.sh create mode 100755 test/ocaml-opam/test_specific_version.sh diff --git a/src/ocaml-opam/devcontainer-feature.json b/src/ocaml-opam/devcontainer-feature.json new file mode 100644 index 000000000..72e9c0bfb --- /dev/null +++ b/src/ocaml-opam/devcontainer-feature.json @@ -0,0 +1,29 @@ +{ + "id": "ocaml-opam", + "version": "1.0.0", + "name": "OCaml (via opam)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ocaml-opam", + "description": "Installation of OCaml that includes a package manager and the compiler itself. Also install some platform tools like a build system, support for your editor, and a few other important ones.", + "options": { + "version": { + "default": "latest", + "description": "Select the OCaml version to install.", + "proposals": [ + "latest", + "5.3.0", + "4.14.2" + ], + "type": "string" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ocamllabs.ocaml-platform" + ] + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} diff --git a/src/ocaml-opam/install.sh b/src/ocaml-opam/install.sh new file mode 100755 index 000000000..6f7fb721a --- /dev/null +++ b/src/ocaml-opam/install.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='ocaml/opam' --option binaryNames='opam' --option version="latest" + + +# Initialise opam +if [ "$VERSION" = "latest" ]; then + su "$_REMOTE_USER" -c "opam init --disable-sandboxing --shell-setup -y" +else + su "$_REMOTE_USER" -c "opam init --disable-sandboxing --shell-setup -y --compiler=\"$VERSION\"" +fi +# Install Platform Tools +su "$_REMOTE_USER" -c "opam install ocaml-lsp-server odoc ocamlformat utop -y" + + +echo 'Done!' diff --git a/src/ocaml-opam/library_scripts.sh b/src/ocaml-opam/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/ocaml-opam/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/ocaml-opam/scenarios.json b/test/ocaml-opam/scenarios.json new file mode 100644 index 000000000..31bc9c412 --- /dev/null +++ b/test/ocaml-opam/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ocaml-opam": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ocaml-opam": { + "version": "4.14.2" + } + } + } +} diff --git a/test/ocaml-opam/test.sh b/test/ocaml-opam/test.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/test/ocaml-opam/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/test/ocaml-opam/test_debian.sh b/test/ocaml-opam/test_debian.sh new file mode 100755 index 000000000..05511e101 --- /dev/null +++ b/test/ocaml-opam/test_debian.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opam is installed" opam --version +check "OCaml is installed" ocamlc --version + +# TODO: check "[WARNING] The environment is not in sync with the current switch." not present + +reportResults diff --git a/test/ocaml-opam/test_specific_version.sh b/test/ocaml-opam/test_specific_version.sh new file mode 100755 index 000000000..279689b1a --- /dev/null +++ b/test/ocaml-opam/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "OCaml version is equal to 4.14.2" sh -c "ocamlc --version | grep '4.14.2'" + +reportResults From f7147f8417bbe96c39d3b644b0d6e489a1c0fc0b Mon Sep 17 00:00:00 2001 From: Benjamin Collet Date: Thu, 6 Mar 2025 10:41:00 +0000 Subject: [PATCH 2/4] Improve tests of ocaml-opam --- src/ocaml-opam/install.sh | 2 +- test/ocaml-opam/test.sh | 9 --------- test/ocaml-opam/test_debian.sh | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) delete mode 100755 test/ocaml-opam/test.sh diff --git a/src/ocaml-opam/install.sh b/src/ocaml-opam/install.sh index 6f7fb721a..958d197ef 100755 --- a/src/ocaml-opam/install.sh +++ b/src/ocaml-opam/install.sh @@ -16,7 +16,7 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='ocaml/opam' --option binaryNames='opam' --option version="latest" + --option repo='ocaml/opam' --option binaryNames='opam' --option version="latest" --option releaseTagRegex='^(?!.*(alpha|beta|rc)).*$' # Initialise opam diff --git a/test/ocaml-opam/test.sh b/test/ocaml-opam/test.sh deleted file mode 100755 index b2873a73c..000000000 --- a/test/ocaml-opam/test.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e - -source dev-container-features-test-lib - -check "something is installed" something --version - -reportResults diff --git a/test/ocaml-opam/test_debian.sh b/test/ocaml-opam/test_debian.sh index 05511e101..54a2f4cb8 100755 --- a/test/ocaml-opam/test_debian.sh +++ b/test/ocaml-opam/test_debian.sh @@ -7,6 +7,6 @@ source dev-container-features-test-lib check "opam is installed" opam --version check "OCaml is installed" ocamlc --version -# TODO: check "[WARNING] The environment is not in sync with the current switch." not present +check "no opam warning" sh -c "test $(opam switch 2>&1 | grep --count 'WARNING') -eq 0" reportResults From 38187fc54262222779f4252d96ebb697304f5f81 Mon Sep 17 00:00:00 2001 From: Benjamin Collet Date: Thu, 6 Mar 2025 15:20:39 +0000 Subject: [PATCH 3/4] Add option to skip install of OCaml Platform Tools --- src/ocaml-opam/devcontainer-feature.json | 5 +++++ src/ocaml-opam/install.sh | 13 ++++++++----- test/ocaml-opam/scenarios.json | 8 ++++++++ test/ocaml-opam/test_debian.sh | 1 + test/ocaml-opam/test_no_platform_tools.sh | 9 +++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100755 test/ocaml-opam/test_no_platform_tools.sh diff --git a/src/ocaml-opam/devcontainer-feature.json b/src/ocaml-opam/devcontainer-feature.json index 72e9c0bfb..4dd526936 100644 --- a/src/ocaml-opam/devcontainer-feature.json +++ b/src/ocaml-opam/devcontainer-feature.json @@ -14,6 +14,11 @@ "4.14.2" ], "type": "string" + }, + "installPlatformTools": { + "default": true, + "description": "Install some of the OCaml Platform tools? (UTop, Dune, OCaml-LSP, odoc, OCamlFormat)", + "type": "boolean" } }, "customizations": { diff --git a/src/ocaml-opam/install.sh b/src/ocaml-opam/install.sh index 958d197ef..6cb2df993 100755 --- a/src/ocaml-opam/install.sh +++ b/src/ocaml-opam/install.sh @@ -20,13 +20,16 @@ $nanolayer_location \ # Initialise opam -if [ "$VERSION" = "latest" ]; then - su "$_REMOTE_USER" -c "opam init --disable-sandboxing --shell-setup -y" -else - su "$_REMOTE_USER" -c "opam init --disable-sandboxing --shell-setup -y --compiler=\"$VERSION\"" +flags=() +if [ "$VERSION" != "latest" ]; then + flags+=(--compiler="$VERSION") fi +su "$_REMOTE_USER" -c "opam init --disable-sandboxing --shell-setup -y ${flags[*]}" + # Install Platform Tools -su "$_REMOTE_USER" -c "opam install ocaml-lsp-server odoc ocamlformat utop -y" +if [ "$INSTALLPLATFORMTOOLS" = "true" ]; then + su "$_REMOTE_USER" -c "opam install ocaml-lsp-server odoc ocamlformat utop -y" +fi echo 'Done!' diff --git a/test/ocaml-opam/scenarios.json b/test/ocaml-opam/scenarios.json index 31bc9c412..d83b39ddb 100644 --- a/test/ocaml-opam/scenarios.json +++ b/test/ocaml-opam/scenarios.json @@ -12,5 +12,13 @@ "version": "4.14.2" } } + }, + "test_no_platform_tools": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ocaml-opam": { + "installPlatformTools": false + } + } } } diff --git a/test/ocaml-opam/test_debian.sh b/test/ocaml-opam/test_debian.sh index 54a2f4cb8..8237241c3 100755 --- a/test/ocaml-opam/test_debian.sh +++ b/test/ocaml-opam/test_debian.sh @@ -6,6 +6,7 @@ source dev-container-features-test-lib check "opam is installed" opam --version check "OCaml is installed" ocamlc --version +check "UTop is installed" utop -version check "no opam warning" sh -c "test $(opam switch 2>&1 | grep --count 'WARNING') -eq 0" diff --git a/test/ocaml-opam/test_no_platform_tools.sh b/test/ocaml-opam/test_no_platform_tools.sh new file mode 100755 index 000000000..69e7b5c19 --- /dev/null +++ b/test/ocaml-opam/test_no_platform_tools.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "no utop" sh -c "! command -v utop" + +reportResults From 98133a4a083833e5ec3b765fa0dbb93deed805e1 Mon Sep 17 00:00:00 2001 From: Benjamin Collet Date: Wed, 12 Mar 2025 16:09:10 +0100 Subject: [PATCH 4/4] Update src/ocaml-opam/install.sh Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Benjamin Collet --- src/ocaml-opam/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ocaml-opam/install.sh b/src/ocaml-opam/install.sh index 6cb2df993..632682f60 100755 --- a/src/ocaml-opam/install.sh +++ b/src/ocaml-opam/install.sh @@ -15,7 +15,7 @@ ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1" \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ --option repo='ocaml/opam' --option binaryNames='opam' --option version="latest" --option releaseTagRegex='^(?!.*(alpha|beta|rc)).*$'