From 362d9106a5e5b3a032a58d37fa9b2332a5b2f5e4 Mon Sep 17 00:00:00 2001 From: Fazle Adyuta Utomo Date: Sat, 23 Aug 2025 08:40:07 +0000 Subject: [PATCH 1/3] feat(protoc): add protoc feature --- src/protoc/NOTES.md | 5 ++ src/protoc/devcontainer-feature.json | 20 +++++ src/protoc/install.sh | 122 +++++++++++++++++++++++++++ src/protoc/utils.sh | 88 +++++++++++++++++++ test/protoc/scenarios.json | 8 ++ test/protoc/test.sh | 9 ++ 6 files changed, 252 insertions(+) create mode 100644 src/protoc/NOTES.md create mode 100644 src/protoc/devcontainer-feature.json create mode 100644 src/protoc/install.sh create mode 100644 src/protoc/utils.sh create mode 100644 test/protoc/scenarios.json create mode 100644 test/protoc/test.sh diff --git a/src/protoc/NOTES.md b/src/protoc/NOTES.md new file mode 100644 index 0000000..4a36dc0 --- /dev/null +++ b/src/protoc/NOTES.md @@ -0,0 +1,5 @@ +## OS Support + +This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. + +`bash` is required to execute the `install.sh` script. \ No newline at end of file diff --git a/src/protoc/devcontainer-feature.json b/src/protoc/devcontainer-feature.json new file mode 100644 index 0000000..3815bc7 --- /dev/null +++ b/src/protoc/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "name": "Protoc", + "id": "protoc", + "version": "1.0.2", + "documentationURL": "http://github.com/omoxyz/devcontainer-features/tree/main/src/protoc", + "description": "Install Protoc protocol buffer compiler.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/git" + ] +} \ No newline at end of file diff --git a/src/protoc/install.sh b/src/protoc/install.sh new file mode 100644 index 0000000..7e4fe3d --- /dev/null +++ b/src/protoc/install.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Scripts must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +source ./utils.sh + +PROTOC_VERSION=${VERSION:-"latest"} +INSTALL_DIRECTLY_FROM_GITHUB_RELEASE=${INSTALLDIRECTLYFROMGITHUBRELEASE:-"true"} +GITHUB_REPO=https://github.com/google/protobuf + +# Exit immediately if a command exits with a non-zero status. +set -e + +apt_get_update + +# Clean up +rm -rf /var/lib/apt/lists/* + +export DEBIAN_FRONTEND=noninteractive + +get_github_filename() { + local version=$1 + local arch=$2 + echo "protoc-${version}-linux-${arch}.zip" +} + +install_from_github() { + local version_list=$(git ls-remote --tags ${GITHUB_REPO}) + + # Get 2 latest appropriate versions + versions=($(find_latest_versions $PROTOC_VERSION version_list "tags/v")) + if [ $? -eq 1 ]; then + echo "Can't find appropriate version" + exit 1 + fi + + latest_version=${versions[0]} + prev_version=${versions[1]} + + echo "Downloading protoc v${latest_version}...." + + check_packages wget + + # Get architecture + local arch=$(dpkg --print-architecture) + + # Map to generic architecture + case "$arch" in + amd64) + arch="x86_64" + ;; + i386) + arch="x86_32" + ;; + arm64) + arch="aarch64" + ;; + armhf) + arch="arm" + ;; + ppc64el) + arch="ppc64le" + ;; + s390x) + arch="s390x" + ;; + *) + echo "Unknown architecture $arch." + exit 1 + ;; + esac + + local filename=$(get_github_filename $latest_version $arch) + + set +e + + # Create temporary directory + mkdir -p /tmp/protoc + pushd /tmp/protoc + + # Download zip file + wget ${GITHUB_REPO}/releases/download/v${latest_version}/${filename} + local exit_code=$? + + set -e + + if [ "$exit_code" != "0" ]; then + # Handle situation where git tags are ahead of what was is available to actually download + echo "(!) protoc version ${latest_version} failed to download. Attempting to fall back to ${prev_version} to retry..." + filename=$(get_github_filename $prev_version $arch) + wget ${GITHUB_REPO}/releases/download/v${prev_version}/${filename} + fi + + unzip /tmp/protoc/${filename} -d /tmp/protoc + + # Install bin/ + if [[ -d /tmp/protoc/bin ]]; then + echo "Installing binaries to /usr/local/bin/..." + cp -r /tmp/protoc/bin/* /usr/local/bin/ + fi + + # Move include/ + if [[ -d /tmp/protoc/include ]]; then + echo "Installing headers to /usr/local/include/..." + cp -r /tmp/protoc/include/* /usr/local/include/ + fi + + # Remove temporary directory + popd + rm -rf /tmp/protoc +} + +# Install curl, unzip if missing +check_packages curl ca-certificates unzip git + +install_from_github + +# Clean up +rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/src/protoc/utils.sh b/src/protoc/utils.sh new file mode 100644 index 0000000..053090a --- /dev/null +++ b/src/protoc/utils.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Refresh the local package index if no package list entries are stored on the system. +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + for pkg in "$@"; do + # Check if it's a command in PATH + if command -v "$pkg" &> /dev/null; then + echo "[OK] $pkg found in PATH" + continue + fi + + # Check if it's a Debian/Ubuntu package installed + if dpkg -s "$pkg" &> /dev/null; then + echo "[OK] $pkg package installed" + continue + fi + + # If not found, install package + echo "$pkg not found. Installing..." + apt_get_update + apt-get install -y --no-install-recommends $pkg + + done +} + +# Find 2 latest versions that appropriate to requested version +find_latest_versions() { + local requested_version=$1 + local version_list=${!2} + + # Version prefix such as "tags/v" + local prefix_regex=${3:-''} + + # Version number part separator such as "." in "1.0.0" + local separator=${4:-"."} + local escaped_separator=${separator//./\\.} + + local suffix_regex=${5:-''} + + # Format and sort version list + local version_regex="${prefix_regex}\\K[0-9]+(${escaped_separator}[0-9]+){0,2}${suffix_regex}$" + version_list="$(printf "%s\n" "${version_list[@]}" | grep -oP $version_regex| tr -d ' ' | tr $separator '.' | sort -rV)" + + if [ "${requested_version}" = "latest" ]; then + echo "$(echo "${version_list}" | head -n 2)" + else + # Try to get latest matching version + + set +e + local regex="^" + + # Get major version or exit + local major="$(echo "${requested_version}" | grep -oE '^[0-9]+')" + if [ $major != '' ]; then + regex="${regex}${major}" + else + echo "Invalid version \"${requested_version}\". Use \"latest\" or MAJOR[.MINOR][.PATCH]" + return 1 + fi + + # Get minor number or accept any + local minor="$(echo "${requested_version}" | grep -oP '^[0-9]+\.\K[0-9]+')" + regex="${regex}$([ "$minor" != '' ] && echo "${escaped_separator}${minor}" || echo "(${escaped_separator}[0-9]+)?")" + + + # Get patch number or accept any + local patch="$(echo "${requested_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+')" + regex="${regex}$([ "$patch" != '' ] && echo "${escaped_separator}${patch}" || echo "(${escaped_separator}[0-9]+)?")" + set -e + + echo "$(echo "${version_list}" | grep -E -m 2 "^${regex}$")" + fi +} + +get_apt_versions() { + package="$1" + apt list -a "$package" 2>/dev/null \ + | awk -F' ' 'NR>1 {print $2}' \ + | sort -rV +} \ No newline at end of file diff --git a/test/protoc/scenarios.json b/test/protoc/scenarios.json new file mode 100644 index 0000000..eb2c92f --- /dev/null +++ b/test/protoc/scenarios.json @@ -0,0 +1,8 @@ +{ + "test": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "protoc": {} + } + } +} \ No newline at end of file diff --git a/test/protoc/test.sh b/test/protoc/test.sh new file mode 100644 index 0000000..f37d7fe --- /dev/null +++ b/test/protoc/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "protoc version" protoc --version + +reportResults \ No newline at end of file From f7c69c3e4135773177e559d3196adfa403e76eeb Mon Sep 17 00:00:00 2001 From: Fazle Adyuta Utomo Date: Sat, 23 Aug 2025 08:40:33 +0000 Subject: [PATCH 2/3] docs(readme): update features list --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 55366de..abb6df4 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ Custom features for dev containers used in development of Omoxyz software projec ## Features -- **Lefthook** (`lefthook`) – fast polyglot Git hooks manager to automate code checks, formatting, and tests before commits and pushes. -- **Air** (`go-air`) - live reloader for Go apps +- **Lefthook** ([lefthook](./src/lefthook/README.md)) – fast polyglot Git hooks manager to automate code checks, formatting, and tests before commits and pushes. +- **Air** ([go-air](./src/go-air/README.md)) - live reloader for Go apps +- **Protoc** ([protoc](./src/protoc/README.md)) ## Usage From 707b2d0b5d76c908dd5824ee826f06bc9c3b7709 Mon Sep 17 00:00:00 2001 From: Fazle Adyuta Utomo Date: Sat, 23 Aug 2025 08:41:30 +0000 Subject: [PATCH 3/3] chore(protoc): fix version number to 1.0.0 --- src/protoc/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protoc/devcontainer-feature.json b/src/protoc/devcontainer-feature.json index 3815bc7..86714ec 100644 --- a/src/protoc/devcontainer-feature.json +++ b/src/protoc/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Protoc", "id": "protoc", - "version": "1.0.2", + "version": "1.0.0", "documentationURL": "http://github.com/omoxyz/devcontainer-features/tree/main/src/protoc", "description": "Install Protoc protocol buffer compiler.", "options": {