diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh new file mode 100644 index 000000000..5cc7babab --- /dev/null +++ b/src/claude-code/bootstrap.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +set -e + +# Parse command line arguments +TARGET="$1" # Optional target parameter + +# Validate target if provided +if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then + echo "Usage: $0 [stable|latest|VERSION]" >&2 + exit 1 +fi + +GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +DOWNLOAD_DIR="$HOME/.claude/downloads" + +# Check for required dependencies +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "Either curl or wget is required but neither is installed" >&2 + exit 1 +fi + +# Check if jq is available (optional) +HAS_JQ=false +if command -v jq >/dev/null 2>&1; then + HAS_JQ=true +fi + +# Download function that works with both curl and wget +download_file() { + local url="$1" + local output="$2" + + if [ "$DOWNLOADER" = "curl" ]; then + if [ -n "$output" ]; then + curl -fsSL -o "$output" "$url" + else + curl -fsSL "$url" + fi + elif [ "$DOWNLOADER" = "wget" ]; then + if [ -n "$output" ]; then + wget -q -O "$output" "$url" + else + wget -q -O - "$url" + fi + else + return 1 + fi +} + +# Simple JSON parser for extracting checksum when jq is not available +get_checksum_from_manifest() { + local json="$1" + local platform="$2" + + # Normalize JSON to single line and extract checksum + json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g') + + # Extract checksum for platform using bash regex + if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + return 1 +} + +# Detect platform +case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;; + *) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac, +# download the native arm64 binary instead of the x64 one +if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi +fi + +# Check for musl on Linux and adjust platform accordingly +if [ "$os" = "linux" ]; then + if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then + platform="linux-${arch}-musl" + else + platform="linux-${arch}" + fi +else + platform="${os}-${arch}" +fi +mkdir -p "$DOWNLOAD_DIR" + +# Always download latest version (which has the most up-to-date installer) +version=$(download_file "$GCS_BUCKET/latest") + +# Download manifest and extract checksum +manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json") + +# Use jq if available, otherwise fall back to pure bash parsing +if [ "$HAS_JQ" = true ]; then + checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty") +else + checksum=$(get_checksum_from_manifest "$manifest_json" "$platform") +fi + +# Validate checksum format (SHA256 = 64 hex characters) +if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then + echo "Platform $platform not found in manifest" >&2 + exit 1 +fi + +# Download and verify +binary_path="$DOWNLOAD_DIR/claude-$version-$platform" +if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then + echo "Download failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +# Pick the right checksum tool +if [ "$os" = "darwin" ]; then + actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1) +else + actual=$(sha256sum "$binary_path" | cut -d' ' -f1) +fi + +if [ "$actual" != "$checksum" ]; then + echo "Checksum verification failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +chmod +x "$binary_path" + +# Run claude install to set up launcher and shell integration +echo "Setting up Claude Code..." +"$binary_path" install ${TARGET:+"$TARGET"} + +# Clean up downloaded file +rm -f "$binary_path" + +echo "" +echo "✅ Installation complete!" +echo "" diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 281bec53c..1e970d379 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,7 +1,7 @@ { "id": "claude-code", - "version": "1.0.0", - "name": "claude-code (via npm)", + "version": "2.0.0", + "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", "options": { diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index adc2d3820..ecc6b50df 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -2,20 +2,13 @@ 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/npm-package:1.0.4" \ - --option package='@anthropic-ai/claude-code' --option version="$VERSION" +# Install via Anthropic's recommended method +# Script downloaded from them: +# https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh +bash "$(dirname "$0")/bootstrap.sh" "$VERSION" + +# Copy the binary to /usr/local/bin for global access +# It's installed in /root/.local/bin/claude and there's no option to override the install location +cp "$HOME/.local/bin/claude" /usr/local/bin/claude echo 'Done!' diff --git a/src/claude-code/library_scripts.sh b/src/claude-code/library_scripts.sh deleted file mode 100644 index f6d0760d7..000000000 --- a/src/claude-code/library_scripts.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/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 - -}