diff --git a/.changeset/split-sandbox-modular.md b/.changeset/split-sandbox-modular.md new file mode 100644 index 0000000..bac7d7a --- /dev/null +++ b/.changeset/split-sandbox-modular.md @@ -0,0 +1,15 @@ +--- +bump: minor +--- + +Split sandbox into modular per-language components + +Added modular architecture under ubuntu/24.04/ with: +- Per-language install.sh scripts and Dockerfiles (16 languages) +- essentials-sandbox: minimal image for git identity tools +- full-sandbox: complete image built on top of essentials +- Shared common.sh utilities + +Each language can now be installed standalone on Ubuntu 24.04 or built +as an independent Docker image, enabling configurable disk usage and +parallel CI/CD builds. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6381ed2..3942067 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: paths: - 'Dockerfile' - 'scripts/**' + - 'ubuntu/**' - '.github/workflows/release.yml' - '.changeset/**' - 'VERSION' @@ -93,7 +94,7 @@ jobs: # Check if there are code changes (Dockerfile, scripts, etc.) git fetch origin "$GITHUB_BASE_REF" 2>/dev/null || true - CODE_CHANGES=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" | grep -E '^(Dockerfile|scripts/|\.github/workflows/)' || true) + CODE_CHANGES=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" | grep -E '^(Dockerfile|scripts/|ubuntu/|\.github/workflows/)' || true) if [ -z "$CODE_CHANGES" ]; then echo "No code changes detected, changeset not required" @@ -201,19 +202,40 @@ jobs: echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "bumped=true" >> $GITHUB_OUTPUT - # === DETECT CHANGES === + # === DETECT CHANGES (per-image granularity) === detect-changes: runs-on: ubuntu-24.04 needs: [apply-changesets, version-bump] # Always run, but wait for version jobs if they're running if: always() && (needs.apply-changesets.result == 'success' || needs.apply-changesets.result == 'skipped') && (needs.version-bump.result == 'success' || needs.version-bump.result == 'skipped') outputs: + # Legacy change detection docker-changed: ${{ steps.changes.outputs.docker }} scripts-changed: ${{ steps.changes.outputs.scripts }} + ubuntu-changed: ${{ steps.changes.outputs.ubuntu }} workflow-changed: ${{ steps.changes.outputs.workflow }} version-changed: ${{ steps.changes.outputs.version }} should-build: ${{ steps.should-build.outputs.result }} version: ${{ steps.version.outputs.version }} + # Per-image change detection + js-changed: ${{ steps.image-changes.outputs.js }} + essentials-changed: ${{ steps.image-changes.outputs.essentials }} + full-changed: ${{ steps.image-changes.outputs.full }} + common-changed: ${{ steps.image-changes.outputs.common }} + # Per-language change detection + python-changed: ${{ steps.language-changes.outputs.python }} + go-changed: ${{ steps.language-changes.outputs.go }} + rust-changed: ${{ steps.language-changes.outputs.rust }} + java-changed: ${{ steps.language-changes.outputs.java }} + kotlin-changed: ${{ steps.language-changes.outputs.kotlin }} + ruby-changed: ${{ steps.language-changes.outputs.ruby }} + php-changed: ${{ steps.language-changes.outputs.php }} + perl-changed: ${{ steps.language-changes.outputs.perl }} + swift-changed: ${{ steps.language-changes.outputs.swift }} + lean-changed: ${{ steps.language-changes.outputs.lean }} + rocq-changed: ${{ steps.language-changes.outputs.rocq }} + cpp-changed: ${{ steps.language-changes.outputs.cpp }} + assembly-changed: ${{ steps.language-changes.outputs.assembly }} steps: - uses: actions/checkout@v4 @@ -249,6 +271,8 @@ jobs: # Get changed files CHANGED_FILES=$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD) + echo "Changed files:" + echo "$CHANGED_FILES" # Check for Docker-related changes if echo "$CHANGED_FILES" | grep -qE '^Dockerfile$'; then @@ -264,6 +288,13 @@ jobs: echo "scripts=false" >> $GITHUB_OUTPUT fi + # Check for ubuntu/ modular scripts changes + if echo "$CHANGED_FILES" | grep -qE '^ubuntu/'; then + echo "ubuntu=true" >> $GITHUB_OUTPUT + else + echo "ubuntu=false" >> $GITHUB_OUTPUT + fi + # Check for workflow changes if echo "$CHANGED_FILES" | grep -qE '^\.github/workflows/'; then echo "workflow=true" >> $GITHUB_OUTPUT @@ -271,155 +302,957 @@ jobs: echo "workflow=false" >> $GITHUB_OUTPUT fi - # Check for VERSION file changes - if echo "$CHANGED_FILES" | grep -qE '^VERSION$'; then - echo "version=true" >> $GITHUB_OUTPUT - else - echo "version=false" >> $GITHUB_OUTPUT - fi + # Check for VERSION file changes + if echo "$CHANGED_FILES" | grep -qE '^VERSION$'; then + echo "version=true" >> $GITHUB_OUTPUT + else + echo "version=false" >> $GITHUB_OUTPUT + fi + + # Save changed files for per-image detection + echo "$CHANGED_FILES" > /tmp/changed-files.txt + + - name: Detect per-image changes + id: image-changes + run: | + CHANGED_FILES=$(cat /tmp/changed-files.txt) + + # JS sandbox changes + if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/js/'; then + echo "js=true" >> $GITHUB_OUTPUT + else + echo "js=false" >> $GITHUB_OUTPUT + fi + + # Essentials sandbox changes + if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/essentials-sandbox/'; then + echo "essentials=true" >> $GITHUB_OUTPUT + else + echo "essentials=false" >> $GITHUB_OUTPUT + fi + + # Full sandbox changes (full-sandbox dir, root Dockerfile, or scripts) + if echo "$CHANGED_FILES" | grep -qE '^(ubuntu/24\.04/full-sandbox/|Dockerfile$|scripts/)'; then + echo "full=true" >> $GITHUB_OUTPUT + else + echo "full=false" >> $GITHUB_OUTPUT + fi + + # Common.sh changes (affects all images) + if echo "$CHANGED_FILES" | grep -qE '^ubuntu/24\.04/common\.sh$'; then + echo "common=true" >> $GITHUB_OUTPUT + else + echo "common=false" >> $GITHUB_OUTPUT + fi + + - name: Detect per-language changes + id: language-changes + run: | + CHANGED_FILES=$(cat /tmp/changed-files.txt) + + for lang in python go rust java kotlin ruby php perl swift lean rocq cpp assembly; do + if echo "$CHANGED_FILES" | grep -qE "^ubuntu/24\.04/${lang}/"; then + echo "${lang}=true" >> $GITHUB_OUTPUT + else + echo "${lang}=false" >> $GITHUB_OUTPUT + fi + done + + - name: Determine if build is needed + id: should-build + run: | + # For workflow_dispatch with bump-and-release, always build + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "${{ github.event.inputs.release_mode }}" = "bump-and-release" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: manual bump-and-release" + exit 0 + elif [ "${{ github.event.inputs.release_mode }}" = "build-only" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: manual build-only" + exit 0 + fi + fi + + # Trigger build on any relevant changes + if [ "${{ steps.changes.outputs.docker }}" = "true" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: Dockerfile changes" + elif [ "${{ steps.changes.outputs.scripts }}" = "true" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: scripts changes" + elif [ "${{ steps.changes.outputs.ubuntu }}" = "true" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: ubuntu modular scripts changes" + elif [ "${{ steps.changes.outputs.workflow }}" = "true" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: workflow changes" + elif [ "${{ steps.changes.outputs.version }}" = "true" ]; then + echo "result=true" >> $GITHUB_OUTPUT + echo "Build triggered by: VERSION file changes" + else + echo "result=false" >> $GITHUB_OUTPUT + echo "No build needed: no relevant changes detected" + fi + + # === BUILD AND TEST DOCKER IMAGE (PR) === + docker-build-test: + runs-on: ubuntu-24.04 + needs: [detect-changes, version-check, changeset-check] + # Use always() to prevent implicit success() check from skipping this job + # when upstream jobs are skipped (see docs/case-studies/issue-23) + if: | + always() && + needs.detect-changes.result == 'success' && + (needs.version-check.result == 'success' || needs.version-check.result == 'skipped') && + (needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && + github.event_name == 'pull_request' && + needs.detect-changes.outputs.should-build == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build layered images (JS -> essentials -> languages -> full) + run: | + echo "=== Building JS sandbox ===" + docker build -f ubuntu/24.04/js/Dockerfile -t sandbox-js . + + echo "" + echo "=== Building essentials sandbox (on JS) ===" + docker build -f ubuntu/24.04/essentials-sandbox/Dockerfile \ + --build-arg JS_IMAGE=sandbox-js -t sandbox-essentials . + + echo "" + echo "=== Building language images (on essentials) ===" + for lang in python go rust java kotlin ruby php perl swift lean rocq; do + echo "" + echo "--- Building ${lang} sandbox ---" + docker build -f "ubuntu/24.04/${lang}/Dockerfile" \ + --build-arg ESSENTIALS_IMAGE=sandbox-essentials \ + -t "sandbox-${lang}" . + done + + echo "" + echo "=== Building full sandbox (multi-stage from all language images) ===" + docker build -f ubuntu/24.04/full-sandbox/Dockerfile \ + --build-arg ESSENTIALS_IMAGE=sandbox-essentials \ + --build-arg PYTHON_IMAGE=sandbox-python \ + --build-arg GO_IMAGE=sandbox-go \ + --build-arg RUST_IMAGE=sandbox-rust \ + --build-arg JAVA_IMAGE=sandbox-java \ + --build-arg KOTLIN_IMAGE=sandbox-kotlin \ + --build-arg RUBY_IMAGE=sandbox-ruby \ + --build-arg PHP_IMAGE=sandbox-php \ + --build-arg PERL_IMAGE=sandbox-perl \ + --build-arg SWIFT_IMAGE=sandbox-swift \ + --build-arg LEAN_IMAGE=sandbox-lean \ + --build-arg ROCQ_IMAGE=sandbox-rocq \ + -t sandbox-test . + + - name: Test JS sandbox + run: | + echo "=== Testing JS sandbox ===" + docker run --rm sandbox-js bash -c '. $HOME/.nvm/nvm.sh && node --version' || echo "Node.js test failed" + docker run --rm sandbox-js bash -c 'export PATH=$HOME/.bun/bin:$PATH && bun --version' || echo "Bun test failed" + docker run --rm sandbox-js bash -c 'export PATH=$HOME/.deno/bin:$PATH && deno --version' || echo "Deno test failed" + echo "=== JS sandbox tests completed ===" + + - name: Test essentials sandbox + run: | + echo "=== Testing essentials sandbox ===" + docker run --rm sandbox-essentials gh --version || echo "GitHub CLI test failed" + docker run --rm sandbox-essentials glab --version || echo "GitLab CLI test failed" + docker run --rm sandbox-essentials gh-setup-git-identity --version || echo "gh-setup-git-identity test failed" + docker run --rm sandbox-essentials glab-setup-git-identity --version || echo "glab-setup-git-identity test failed" + echo "=== Essentials sandbox tests completed ===" + + - name: Test full sandbox + run: | + echo "=== Testing full sandbox ===" + echo "Note: Using entrypoint script which initializes all environments" + + docker run --rm sandbox-test node --version || echo "Node.js test failed" + docker run --rm sandbox-test python --version || echo "Python test failed" + docker run --rm sandbox-test go version || echo "Go test failed" + docker run --rm sandbox-test rustc --version || echo "Rust test failed" + docker run --rm sandbox-test java -version || echo "Java test failed" + docker run --rm sandbox-test bun --version || echo "Bun test failed" + docker run --rm sandbox-test deno --version || echo "Deno test failed" + docker run --rm sandbox-test gh --version || echo "GitHub CLI test failed" + docker run --rm sandbox-test glab --version || echo "GitLab CLI test failed" + docker run --rm sandbox-test gh-setup-git-identity --version || echo "gh-setup-git-identity test failed" + docker run --rm sandbox-test glab-setup-git-identity --version || echo "glab-setup-git-identity test failed" + docker run --rm sandbox-test lean --version || echo "Lean test failed" + docker run --rm sandbox-test perl --version || echo "Perl test failed" + docker run --rm sandbox-test php --version || echo "PHP test failed" + + echo "" + echo "=== All tests completed ===" + + # === BUILD JS SANDBOX (amd64) === + # JS sandbox is the base layer - built first, other images depend on it + build-js-amd64: + runs-on: ubuntu-24.04 + needs: [detect-changes] + if: | + always() && + needs.detect-changes.result == 'success' && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) && + ( + needs.detect-changes.outputs.js-changed == 'true' || + needs.detect-changes.outputs.common-changed == 'true' || + needs.detect-changes.outputs.version-changed == 'true' || + github.event_name == 'workflow_dispatch' + ) + permissions: + contents: read + packages: write + outputs: + built: ${{ steps.result.outputs.built }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push JS sandbox (amd64) + uses: docker/build-push-action@v5 + with: + context: . + file: ubuntu/24.04/js/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-amd64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64 + provenance: false + cache-from: type=gha,scope=js-amd64 + cache-to: type=gha,scope=js-amd64,mode=max + + - name: Mark as built + id: result + run: echo "built=true" >> $GITHUB_OUTPUT + + # === BUILD JS SANDBOX (arm64) === + build-js-arm64: + runs-on: ubuntu-24.04-arm + timeout-minutes: 120 + needs: [detect-changes] + if: | + always() && + needs.detect-changes.result == 'success' && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) && + ( + needs.detect-changes.outputs.js-changed == 'true' || + needs.detect-changes.outputs.common-changed == 'true' || + needs.detect-changes.outputs.version-changed == 'true' || + github.event_name == 'workflow_dispatch' + ) + permissions: + contents: read + packages: write + outputs: + built: ${{ steps.result.outputs.built }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push JS sandbox (arm64) + uses: docker/build-push-action@v5 + with: + context: . + file: ubuntu/24.04/js/Dockerfile + platforms: linux/arm64 + push: true + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-arm64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64 + provenance: false + cache-from: type=gha,scope=js-arm64 + cache-to: type=gha,scope=js-arm64,mode=max + + - name: Mark as built + id: result + run: echo "built=true" >> $GITHUB_OUTPUT + + # === CREATE JS MULTI-ARCH MANIFEST === + js-manifest: + runs-on: ubuntu-24.04 + needs: [detect-changes, build-js-amd64, build-js-arm64] + if: | + always() && + needs.detect-changes.result == 'success' && + needs.build-js-amd64.result == 'success' && + needs.build-js-arm64.result == 'success' + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push JS multi-arch manifests + run: | + VERSION="${{ steps.version.outputs.version }}" + + # GHCR + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:latest + + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION} \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION}-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION}-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-js:${VERSION} + + # Docker Hub + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest + + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION} \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION}-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION}-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-js:${VERSION} + + echo "JS sandbox multi-arch manifests pushed for latest and ${VERSION}" + + # === BUILD ESSENTIALS SANDBOX (amd64) === + # Built on top of JS sandbox - waits for JS to complete (if JS was rebuilt) + build-essentials-amd64: + runs-on: ubuntu-24.04 + needs: [detect-changes, build-js-amd64] + if: | + always() && + needs.detect-changes.result == 'success' && + (needs.build-js-amd64.result == 'success' || needs.build-js-amd64.result == 'skipped') && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) && + ( + needs.detect-changes.outputs.js-changed == 'true' || + needs.detect-changes.outputs.essentials-changed == 'true' || + needs.detect-changes.outputs.common-changed == 'true' || + needs.detect-changes.outputs.scripts-changed == 'true' || + needs.detect-changes.outputs.version-changed == 'true' || + github.event_name == 'workflow_dispatch' + ) + permissions: + contents: read + packages: write + outputs: + built: ${{ steps.result.outputs.built }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine JS base image + id: js-base + run: | + # Use freshly built JS image if JS was rebuilt, otherwise use latest + if [ "${{ needs.build-js-amd64.outputs.built }}" = "true" ]; then + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-amd64" >> $GITHUB_OUTPUT + else + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-amd64" >> $GITHUB_OUTPUT + fi + + - name: Build and push essentials sandbox (amd64) + uses: docker/build-push-action@v5 + with: + context: . + file: ubuntu/24.04/essentials-sandbox/Dockerfile + platforms: linux/amd64 + push: true + build-args: | + JS_IMAGE=${{ steps.js-base.outputs.image }} + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-amd64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64 + provenance: false + cache-from: type=gha,scope=essentials-amd64 + cache-to: type=gha,scope=essentials-amd64,mode=max + + - name: Mark as built + id: result + run: echo "built=true" >> $GITHUB_OUTPUT + + # === BUILD ESSENTIALS SANDBOX (arm64) === + build-essentials-arm64: + runs-on: ubuntu-24.04-arm + timeout-minutes: 120 + needs: [detect-changes, build-js-arm64] + if: | + always() && + needs.detect-changes.result == 'success' && + (needs.build-js-arm64.result == 'success' || needs.build-js-arm64.result == 'skipped') && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) && + ( + needs.detect-changes.outputs.js-changed == 'true' || + needs.detect-changes.outputs.essentials-changed == 'true' || + needs.detect-changes.outputs.common-changed == 'true' || + needs.detect-changes.outputs.scripts-changed == 'true' || + needs.detect-changes.outputs.version-changed == 'true' || + github.event_name == 'workflow_dispatch' + ) + permissions: + contents: read + packages: write + outputs: + built: ${{ steps.result.outputs.built }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine JS base image + id: js-base + run: | + if [ "${{ needs.build-js-arm64.outputs.built }}" = "true" ]; then + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:${{ steps.version.outputs.version }}-arm64" >> $GITHUB_OUTPUT + else + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-js:latest-arm64" >> $GITHUB_OUTPUT + fi + + - name: Build and push essentials sandbox (arm64) + uses: docker/build-push-action@v5 + with: + context: . + file: ubuntu/24.04/essentials-sandbox/Dockerfile + platforms: linux/arm64 + push: true + build-args: | + JS_IMAGE=${{ steps.js-base.outputs.image }} + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-arm64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64 + provenance: false + cache-from: type=gha,scope=essentials-arm64 + cache-to: type=gha,scope=essentials-arm64,mode=max + + - name: Mark as built + id: result + run: echo "built=true" >> $GITHUB_OUTPUT + + # === CREATE ESSENTIALS MULTI-ARCH MANIFEST === + essentials-manifest: + runs-on: ubuntu-24.04 + needs: [detect-changes, build-essentials-amd64, build-essentials-arm64] + if: | + always() && + needs.detect-changes.result == 'success' && + needs.build-essentials-amd64.result == 'success' && + needs.build-essentials-arm64.result == 'success' + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Determine if build is needed - id: should-build + - name: Create and push essentials multi-arch manifests run: | - # For workflow_dispatch with bump-and-release, always build - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - if [ "${{ github.event.inputs.release_mode }}" = "bump-and-release" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: manual bump-and-release" - exit 0 - elif [ "${{ github.event.inputs.release_mode }}" = "build-only" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: manual build-only" - exit 0 - fi - fi - - # Trigger build on any relevant changes - if [ "${{ steps.changes.outputs.docker }}" = "true" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: Dockerfile changes" - elif [ "${{ steps.changes.outputs.scripts }}" = "true" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: scripts changes" - elif [ "${{ steps.changes.outputs.workflow }}" = "true" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: workflow changes" - elif [ "${{ steps.changes.outputs.version }}" = "true" ]; then - echo "result=true" >> $GITHUB_OUTPUT - echo "Build triggered by: VERSION file changes" - else - echo "result=false" >> $GITHUB_OUTPUT - echo "No build needed: no relevant changes detected" - fi + VERSION="${{ steps.version.outputs.version }}" - # === BUILD AND TEST DOCKER IMAGE (PR) === - docker-build-test: + # GHCR + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:latest + + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION} \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION}-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION}-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-essentials:${VERSION} + + # Docker Hub + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest + + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION} \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION} + + echo "Essentials sandbox multi-arch manifests pushed for latest and ${VERSION}" + + # === BUILD LANGUAGE IMAGES (amd64) === + # All language images are built in parallel on top of essentials sandbox + build-languages-amd64: runs-on: ubuntu-24.04 - needs: [detect-changes, version-check, changeset-check] - # Use always() to prevent implicit success() check from skipping this job - # when upstream jobs are skipped (see docs/case-studies/issue-23) + needs: [detect-changes, build-essentials-amd64] + strategy: + fail-fast: false + matrix: + language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] if: | always() && needs.detect-changes.result == 'success' && - (needs.version-check.result == 'success' || needs.version-check.result == 'skipped') && - (needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped') && - github.event_name == 'pull_request' && - needs.detect-changes.outputs.should-build == 'true' + (needs.build-essentials-amd64.result == 'success' || needs.build-essentials-amd64.result == 'skipped') && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) + permissions: + contents: read + packages: write steps: + - name: Check if this language needs building + id: check-lang + run: | + LANG="${{ matrix.language }}" + LANG_CHANGED="${{ needs.detect-changes.outputs[format('{0}-changed', matrix.language)] }}" + ESSENTIALS_CHANGED="${{ needs.detect-changes.outputs.essentials-changed }}" + COMMON_CHANGED="${{ needs.detect-changes.outputs.common-changed }}" + VERSION_CHANGED="${{ needs.detect-changes.outputs.version-changed }}" + JS_CHANGED="${{ needs.detect-changes.outputs.js-changed }}" + + if [ "$LANG_CHANGED" = "true" ] || \ + [ "$ESSENTIALS_CHANGED" = "true" ] || \ + [ "$COMMON_CHANGED" = "true" ] || \ + [ "$JS_CHANGED" = "true" ] || \ + [ "$VERSION_CHANGED" = "true" ] || \ + [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "should_build=true" >> $GITHUB_OUTPUT + echo "Building ${LANG}: change detected or workflow_dispatch" + else + echo "should_build=false" >> $GITHUB_OUTPUT + echo "Skipping ${LANG}: no relevant changes" + fi + - name: Checkout repository + if: steps.check-lang.outputs.should_build == 'true' uses: actions/checkout@v4 + with: + ref: main + + - name: Get latest version + if: steps.check-lang.outputs.should_build == 'true' + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Set up Docker Buildx + if: steps.check-lang.outputs.should_build == 'true' uses: docker/setup-buildx-action@v3 - - name: Build Docker image (test) + - name: Log in to GitHub Container Registry + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine essentials base image + if: steps.check-lang.outputs.should_build == 'true' + id: essentials-base + run: | + if [ "${{ needs.build-essentials-amd64.outputs.built }}" = "true" ]; then + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-amd64" >> $GITHUB_OUTPUT + else + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64" >> $GITHUB_OUTPUT + fi + + - name: Build and push ${{ matrix.language }} sandbox (amd64) + if: steps.check-lang.outputs.should_build == 'true' uses: docker/build-push-action@v5 with: context: . - push: false - tags: ${{ env.GHCR_IMAGE_NAME }}:test - cache-from: type=gha - cache-to: type=gha,mode=max + file: ubuntu/24.04/${{ matrix.language }}/Dockerfile + platforms: linux/amd64 + push: true + build-args: | + ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-amd64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-amd64 + provenance: false + cache-from: type=gha,scope=${{ matrix.language }}-amd64 + cache-to: type=gha,scope=${{ matrix.language }}-amd64,mode=max + + # === BUILD LANGUAGE IMAGES (arm64) === + build-languages-arm64: + runs-on: ubuntu-24.04-arm + timeout-minutes: 120 + needs: [detect-changes, build-essentials-arm64] + strategy: + fail-fast: false + matrix: + language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] + if: | + always() && + needs.detect-changes.result == 'success' && + (needs.build-essentials-arm64.result == 'success' || needs.build-essentials-arm64.result == 'skipped') && + ( + (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || + (github.event_name == 'workflow_dispatch') + ) + permissions: + contents: read + packages: write - - name: Test Docker image + steps: + - name: Check if this language needs building + id: check-lang run: | - echo "Building image for testing..." - docker build -t sandbox-test . + LANG="${{ matrix.language }}" + LANG_CHANGED="${{ needs.detect-changes.outputs[format('{0}-changed', matrix.language)] }}" + ESSENTIALS_CHANGED="${{ needs.detect-changes.outputs.essentials-changed }}" + COMMON_CHANGED="${{ needs.detect-changes.outputs.common-changed }}" + VERSION_CHANGED="${{ needs.detect-changes.outputs.version-changed }}" + JS_CHANGED="${{ needs.detect-changes.outputs.js-changed }}" + + if [ "$LANG_CHANGED" = "true" ] || \ + [ "$ESSENTIALS_CHANGED" = "true" ] || \ + [ "$COMMON_CHANGED" = "true" ] || \ + [ "$JS_CHANGED" = "true" ] || \ + [ "$VERSION_CHANGED" = "true" ] || \ + [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "should_build=true" >> $GITHUB_OUTPUT + echo "Building ${LANG}: change detected or workflow_dispatch" + else + echo "should_build=false" >> $GITHUB_OUTPUT + echo "Skipping ${LANG}: no relevant changes" + fi - echo "" - echo "=== Testing installed tools ===" - echo "Note: Using entrypoint script which initializes all environments" + - name: Checkout repository + if: steps.check-lang.outputs.should_build == 'true' + uses: actions/checkout@v4 + with: + ref: main - echo "" - echo "Testing Node.js..." - docker run --rm sandbox-test node --version || echo "Node.js test failed" + - name: Get latest version + if: steps.check-lang.outputs.should_build == 'true' + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "" - echo "Testing Python..." - docker run --rm sandbox-test python --version || echo "Python test failed" + - name: Set up Docker Buildx + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/setup-buildx-action@v3 - echo "" - echo "Testing Go..." - docker run --rm sandbox-test go version || echo "Go test failed" + - name: Log in to GitHub Container Registry + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - echo "" - echo "Testing Rust..." - docker run --rm sandbox-test rustc --version || echo "Rust test failed" + - name: Log in to Docker Hub + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - echo "" - echo "Testing Java..." - docker run --rm sandbox-test java -version || echo "Java test failed" + - name: Determine essentials base image + if: steps.check-lang.outputs.should_build == 'true' + id: essentials-base + run: | + if [ "${{ needs.build-essentials-arm64.outputs.built }}" = "true" ]; then + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${{ steps.version.outputs.version }}-arm64" >> $GITHUB_OUTPUT + else + echo "image=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64" >> $GITHUB_OUTPUT + fi - echo "" - echo "Testing Bun..." - docker run --rm sandbox-test bun --version || echo "Bun test failed" + - name: Build and push ${{ matrix.language }} sandbox (arm64) + if: steps.check-lang.outputs.should_build == 'true' + uses: docker/build-push-action@v5 + with: + context: . + file: ubuntu/24.04/${{ matrix.language }}/Dockerfile + platforms: linux/arm64 + push: true + build-args: | + ESSENTIALS_IMAGE=${{ steps.essentials-base.outputs.image }} + tags: | + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64 + ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:latest-arm64 + ${{ env.DOCKERHUB_IMAGE_NAME }}-${{ matrix.language }}:${{ steps.version.outputs.version }}-arm64 + provenance: false + cache-from: type=gha,scope=${{ matrix.language }}-arm64 + cache-to: type=gha,scope=${{ matrix.language }}-arm64,mode=max + + # === CREATE LANGUAGE MULTI-ARCH MANIFESTS === + languages-manifest: + runs-on: ubuntu-24.04 + needs: [detect-changes, build-languages-amd64, build-languages-arm64] + strategy: + fail-fast: false + matrix: + language: [python, go, rust, java, kotlin, ruby, php, perl, swift, lean, rocq] + if: | + always() && + needs.detect-changes.result == 'success' && + needs.build-languages-amd64.result == 'success' && + needs.build-languages-arm64.result == 'success' + permissions: + contents: read + packages: write - echo "" - echo "Testing Deno..." - docker run --rm sandbox-test deno --version || echo "Deno test failed" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main - echo "" - echo "Testing GitHub CLI..." - docker run --rm sandbox-test gh --version || echo "GitHub CLI test failed" + - name: Get latest version + id: version + run: | + git pull origin main || true + VERSION=$(cat VERSION | tr -d '[:space:]') + echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "" - echo "Testing GitLab CLI..." - docker run --rm sandbox-test glab --version || echo "GitLab CLI test failed" + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - echo "" - echo "Testing gh-setup-git-identity..." - docker run --rm sandbox-test gh-setup-git-identity --version || echo "gh-setup-git-identity test failed" + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - echo "" - echo "Testing glab-setup-git-identity..." - docker run --rm sandbox-test glab-setup-git-identity --version || echo "glab-setup-git-identity test failed" + - name: Create and push ${{ matrix.language }} multi-arch manifests + run: | + VERSION="${{ steps.version.outputs.version }}" + LANG="${{ matrix.language }}" - echo "" - echo "Testing Lean..." - docker run --rm sandbox-test lean --version || echo "Lean test failed" + # GHCR + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:latest - echo "" - echo "Testing Perl..." - docker run --rm sandbox-test perl --version || echo "Perl test failed" + docker manifest create ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION} \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION}-amd64 \ + --amend ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION}-arm64 + docker manifest push ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}-${LANG}:${VERSION} - echo "" - echo "Testing PHP..." - docker run --rm sandbox-test php --version || echo "PHP test failed" + # Docker Hub + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:latest - echo "" - echo "=== All tests completed ===" + docker manifest create ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION} \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION}-amd64 \ + --amend ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION}-arm64 + docker manifest push ${{ env.DOCKERHUB_IMAGE_NAME }}-${LANG}:${VERSION} - # === BUILD AND PUSH DOCKER IMAGE (MAIN) === + echo "${LANG} sandbox multi-arch manifests pushed for latest and ${VERSION}" + + # === BUILD AND PUSH FULL SANDBOX (MAIN - amd64) === docker-build-push: runs-on: ubuntu-24.04 - needs: [detect-changes] + needs: [detect-changes, build-languages-amd64, build-essentials-amd64] # Run on push to main with changes, OR on workflow_dispatch - # Use always() to prevent implicit success() check from skipping this job - # when upstream jobs are skipped (see docs/case-studies/issue-23) if: | always() && needs.detect-changes.result == 'success' && + (needs.build-essentials-amd64.result == 'success' || needs.build-essentials-amd64.result == 'skipped') && + (needs.build-languages-amd64.result == 'success' || needs.build-languages-amd64.result == 'skipped') && ( (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || (github.event_name == 'workflow_dispatch') @@ -463,6 +1296,29 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Determine base images + id: base-images + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Essentials base image + if [ "${{ needs.build-essentials-amd64.outputs.built }}" = "true" ]; then + echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-amd64" >> $GITHUB_OUTPUT + else + echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-amd64" >> $GITHUB_OUTPUT + fi + + # Language images - use version tag if languages were built, otherwise latest + for lang in python go rust java kotlin ruby php perl swift lean rocq; do + LANG_UPPER=$(echo "$lang" | tr '[:lower:]' '[:upper:]') + # If the language matrix ran successfully, use versioned tag + if [ "${{ needs.build-languages-amd64.result }}" = "success" ]; then + echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:${VERSION}-amd64" >> $GITHUB_OUTPUT + else + echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:latest-amd64" >> $GITHUB_OUTPUT + fi + done + - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 @@ -491,13 +1347,27 @@ jobs: type=sha,prefix= type=raw,value={{date 'YYYYMMDD'}} - - name: Build and push Docker image (amd64) + - name: Build and push full sandbox (amd64) id: build uses: docker/build-push-action@v5 with: context: . + file: ubuntu/24.04/full-sandbox/Dockerfile platforms: linux/amd64 push: true + build-args: | + ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} + PYTHON_IMAGE=${{ steps.base-images.outputs.python }} + GO_IMAGE=${{ steps.base-images.outputs.go }} + RUST_IMAGE=${{ steps.base-images.outputs.rust }} + JAVA_IMAGE=${{ steps.base-images.outputs.java }} + KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} + RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} + PHP_IMAGE=${{ steps.base-images.outputs.php }} + PERL_IMAGE=${{ steps.base-images.outputs.perl }} + SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} + LEAN_IMAGE=${{ steps.base-images.outputs.lean }} + ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} tags: | ${{ steps.meta.outputs.tags }} ${{ steps.meta-amd64.outputs.tags }} @@ -508,18 +1378,15 @@ jobs: # === BUILD AND PUSH ARM64 IMAGE === # Using native ARM64 runner for optimal build performance - # See: docs/case-studies/issue-7/README.md for analysis of the previous timeout - # Reference: https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/ docker-build-push-arm64: runs-on: ubuntu-24.04-arm # Native ARM64 runner (free for public repos since Jan 2025) timeout-minutes: 120 # Safety timeout to prevent runaway builds - needs: [detect-changes, docker-build-push] - # Run on push to main with changes, OR on workflow_dispatch - # Use always() to prevent implicit success() check from skipping this job - # when upstream jobs are skipped (see docs/case-studies/issue-23) + needs: [detect-changes, build-languages-arm64, build-essentials-arm64, docker-build-push] if: | always() && needs.detect-changes.result == 'success' && + (needs.build-essentials-arm64.result == 'success' || needs.build-essentials-arm64.result == 'skipped') && + (needs.build-languages-arm64.result == 'success' || needs.build-languages-arm64.result == 'skipped') && needs.docker-build-push.result == 'success' && ( (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.should-build == 'true') || @@ -563,6 +1430,27 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Determine base images + id: base-images + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Essentials base image + if [ "${{ needs.build-essentials-arm64.outputs.built }}" = "true" ]; then + echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:${VERSION}-arm64" >> $GITHUB_OUTPUT + else + echo "essentials=${{ env.DOCKERHUB_IMAGE_NAME }}-essentials:latest-arm64" >> $GITHUB_OUTPUT + fi + + # Language images + for lang in python go rust java kotlin ruby php perl swift lean rocq; do + if [ "${{ needs.build-languages-arm64.result }}" = "success" ]; then + echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:${VERSION}-arm64" >> $GITHUB_OUTPUT + else + echo "${lang}=${{ env.DOCKERHUB_IMAGE_NAME }}-${lang}:latest-arm64" >> $GITHUB_OUTPUT + fi + done + - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 @@ -578,13 +1466,27 @@ jobs: type=sha,prefix= type=raw,value={{date 'YYYYMMDD'}} - - name: Build and push Docker image (arm64) + - name: Build and push full sandbox (arm64) id: build uses: docker/build-push-action@v5 with: context: . + file: ubuntu/24.04/full-sandbox/Dockerfile platforms: linux/arm64 push: true + build-args: | + ESSENTIALS_IMAGE=${{ steps.base-images.outputs.essentials }} + PYTHON_IMAGE=${{ steps.base-images.outputs.python }} + GO_IMAGE=${{ steps.base-images.outputs.go }} + RUST_IMAGE=${{ steps.base-images.outputs.rust }} + JAVA_IMAGE=${{ steps.base-images.outputs.java }} + KOTLIN_IMAGE=${{ steps.base-images.outputs.kotlin }} + RUBY_IMAGE=${{ steps.base-images.outputs.ruby }} + PHP_IMAGE=${{ steps.base-images.outputs.php }} + PERL_IMAGE=${{ steps.base-images.outputs.perl }} + SWIFT_IMAGE=${{ steps.base-images.outputs.swift }} + LEAN_IMAGE=${{ steps.base-images.outputs.lean }} + ROCQ_IMAGE=${{ steps.base-images.outputs.rocq }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} provenance: false # Prevents unknown/unknown platform in registry @@ -595,8 +1497,6 @@ jobs: docker-manifest: runs-on: ubuntu-24.04 needs: [detect-changes, docker-build-push, docker-build-push-arm64] - # Use always() to prevent implicit success() check from skipping this job - # when upstream jobs are skipped (see docs/case-studies/issue-23) if: | always() && needs.detect-changes.result == 'success' && @@ -681,9 +1581,7 @@ jobs: # === CREATE GITHUB RELEASE === create-release: runs-on: ubuntu-24.04 - needs: [detect-changes, docker-manifest] - # Use always() to prevent implicit success() check from skipping this job - # when upstream jobs are skipped (see docs/case-studies/issue-23) + needs: [detect-changes, docker-manifest, js-manifest, essentials-manifest, languages-manifest] if: | always() && needs.detect-changes.result == 'success' && @@ -723,33 +1621,62 @@ jobs: cat > /tmp/release-notes.md << ENDOFNOTES ## Docker Images - ### GitHub Container Registry (GHCR) - - [\`${GHCR_IMAGE}:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox) (multi-arch) - - [\`${GHCR_IMAGE}:${VERSION}-amd64\`](https://github.com/${REPO}/pkgs/container/sandbox) (AMD64) - - [\`${GHCR_IMAGE}:${VERSION}-arm64\`](https://github.com/${REPO}/pkgs/container/sandbox) (ARM64) - - [\`${GHCR_IMAGE}:latest\`](https://github.com/${REPO}/pkgs/container/sandbox) (multi-arch) - - ### Docker Hub + ### Full Sandbox (konard/sandbox) - [\`${DOCKERHUB_IMAGE}:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) (multi-arch) - [\`${DOCKERHUB_IMAGE}:${VERSION}-amd64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) (AMD64) - [\`${DOCKERHUB_IMAGE}:${VERSION}-arm64\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) (ARM64) - - [\`${DOCKERHUB_IMAGE}:latest\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) (multi-arch) - ## Quick Start + ### Essentials Sandbox (konard/sandbox-essentials) + - [\`${DOCKERHUB_IMAGE}-essentials:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-essentials) (multi-arch) + + ### JS Sandbox (konard/sandbox-js) + - [\`${DOCKERHUB_IMAGE}-js:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-js) (multi-arch) + + ### Language Sandboxes + - [\`${DOCKERHUB_IMAGE}-python:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-python) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-go:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-go) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-rust:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rust) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-java:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-java) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-kotlin:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-kotlin) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-ruby:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-ruby) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-php:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-php) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-perl:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-perl) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-swift:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-swift) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-lean:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-lean) (multi-arch) + - [\`${DOCKERHUB_IMAGE}-rocq:${VERSION}\`](https://hub.docker.com/r/${DOCKERHUB_IMAGE}-rocq) (multi-arch) + + ### GitHub Container Registry (GHCR) + - [\`${GHCR_IMAGE}:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox) (multi-arch) + - [\`${GHCR_IMAGE}-essentials:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-essentials) (multi-arch) + - [\`${GHCR_IMAGE}-js:${VERSION}\`](https://github.com/${REPO}/pkgs/container/sandbox-js) (multi-arch) + + ## Architecture - Pull from GHCR: - \`\`\`sh - docker pull ${GHCR_IMAGE}:${VERSION} \`\`\` + JS sandbox (konard/sandbox-js) + → Essentials sandbox (konard/sandbox-essentials) + ├─ sandbox-python ├─ sandbox-go ├─ sandbox-rust + ├─ sandbox-java ├─ sandbox-kotlin ├─ sandbox-ruby + ├─ sandbox-php ├─ sandbox-perl ├─ sandbox-swift + ├─ sandbox-lean └─ sandbox-rocq + → Full sandbox (konard/sandbox) [merges all language images] + \`\`\` + + ## Quick Start Pull from Docker Hub: \`\`\`sh docker pull ${DOCKERHUB_IMAGE}:${VERSION} \`\`\` + Pull from GHCR: + \`\`\`sh + docker pull ${GHCR_IMAGE}:${VERSION} + \`\`\` + ## Links - - [GHCR Package](https://github.com/${REPO}/pkgs/container/sandbox) - [Docker Hub](https://hub.docker.com/r/${DOCKERHUB_IMAGE}) + - [GHCR Package](https://github.com/${REPO}/pkgs/container/sandbox) Released on ${DATE} ENDOFNOTES diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0370a47..12ddfd2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -77,55 +77,179 @@ See [Case Study: Docker ARM64 Build Timeout](docs/case-studies/issue-7/README.md ## Build Pipeline +The CI/CD pipeline uses per-image change detection for efficiency. Only images whose +scripts or Dockerfiles changed are rebuilt. Unchanged images reuse the latest published version. + +All language images are built in parallel, and the full sandbox assembles them via +multi-stage `COPY --from` once all are ready. + ``` -┌─────────────────┐ -│ detect-changes │ -│ (ubuntu-latest) │ -└────────┬────────┘ +┌──────────────────┐ +│ detect-changes │ (per-image + per-language granularity) +│ (ubuntu-latest) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ ← built first (base layer) +│ build-js │ +│ (amd64 + arm64) │ (parallel per arch) +└────────┬─────────┘ + │ + ▼ +┌────────────────────────┐ +│ build-essentials │ ← built on JS sandbox +│ (amd64 + arm64) │ (parallel per arch) +└────────┬───────────────┘ │ - ├─────────────────────────┐ - │ │ - ▼ ▼ -┌─────────────────┐ ┌─────────────────────┐ -│docker-build-push│ │docker-build-push- │ -│ (amd64) │ │ arm64 │ -│ ubuntu-latest │ │ ubuntu-24.04-arm │ -└────────┬────────┘ │ (NATIVE - NO EMU) │ - │ └──────────┬──────────┘ - │ │ - └──────────┬──────────────┘ - │ - ▼ - ┌─────────────────┐ - │ docker-manifest │ - │ (multi-arch) │ - └─────────────────┘ + ▼ +┌────────────────────────────────────────────────────┐ +│ build-languages (matrix: 11 languages) │ ← ALL in parallel +│ python, go, rust, java, kotlin, ruby, php, perl, │ +│ swift, lean, rocq │ +│ (amd64 + arm64 per language) │ +└────────────────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────┐ +│ docker-build-push │ ← full sandbox: COPY --from all language images +│ (amd64 + arm64) │ (multi-stage assembly, waits for all languages) +└────────┬───────────────┘ + │ + ▼ +┌──────────────────┐ +│ manifests │ ← multi-arch manifests for js, essentials, languages, full +│ (multi-arch) │ +└──────────────────┘ ``` +Each image only rebuilds if its own scripts/Dockerfiles changed, or if a dependency +(common.sh, essentials) changed. The full sandbox uses `COPY --from` to merge +pre-built language runtimes from all language images, plus `apt install` for +system-level packages (.NET, R, C/C++, Assembly). + ## File Structure ``` sandbox/ ├── .github/ │ └── workflows/ -│ └── release.yml # CI/CD workflow -├── docs/ -│ └── case-studies/ -│ └── issue-7/ # ARM64 timeout analysis +│ └── release.yml # CI/CD workflow +├── ubuntu/ +│ └── 24.04/ +│ ├── common.sh # Shared functions for all install scripts +│ ├── js/ # JavaScript/TypeScript (Node.js, Bun, Deno) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── python/ # Python (Pyenv) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── go/ # Go +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── rust/ # Rust (rustup) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── java/ # Java (SDKMAN, Temurin) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── kotlin/ # Kotlin (SDKMAN) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── dotnet/ # .NET SDK 8.0 +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── r/ # R language +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── ruby/ # Ruby (rbenv) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── php/ # PHP 8.3 (Homebrew) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── perl/ # Perl (Perlbrew) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── swift/ # Swift 6.x +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── lean/ # Lean (elan) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── rocq/ # Rocq/Coq (Opam) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── cpp/ # C/C++ (CMake, Clang, LLVM) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── assembly/ # Assembly (NASM, FASM) +│ │ ├── install.sh +│ │ └── Dockerfile +│ ├── essentials-sandbox/ # Minimal sandbox (git identity tools) +│ │ ├── install.sh +│ │ └── Dockerfile +│ └── full-sandbox/ # Complete sandbox (all languages) +│ ├── install.sh +│ └── Dockerfile ├── scripts/ -│ └── ... # Build scripts -├── data/ -│ └── ... # Data files -├── experiments/ -│ └── ... # Experimental scripts -├── Dockerfile # Main container definition -├── README.md # Project overview -├── ARCHITECTURE.md # This file -├── REQUIREMENTS.md # Project requirements -├── LICENSE # MIT License -└── package.json # Node.js metadata +│ ├── ubuntu-24-server-install.sh # Legacy full installation script +│ ├── entrypoint.sh # Container entrypoint +│ ├── measure-disk-space.sh # Disk space measurement +│ └── ... # Other scripts +├── docs/ +│ └── case-studies/ # Case studies +├── data/ # Data files +├── experiments/ # Experimental scripts +├── Dockerfile # Root Dockerfile (full sandbox) +├── README.md # Project overview +├── ARCHITECTURE.md # This file +├── REQUIREMENTS.md # Project requirements +└── LICENSE # MIT License ``` +## Modular Design + +The sandbox follows a modular architecture where all language images depend on +`essentials-sandbox`, and the full sandbox assembles them via multi-stage `COPY --from`: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ JS sandbox (konard/sandbox-js) │ +│ └─ Node.js, Bun, Deno, npm │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Essentials sandbox (konard/sandbox-essentials) │ +│ └─ + git, gh, glab, identity tools, dev libraries │ +└──┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬────┬──┬─┘ + │ │ │ │ │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ + Python Go Rust Java Kotlin Ruby PHP Perl Swift Lean Rocq + │ │ │ │ │ │ │ │ │ │ + └──────┴──────┴──────┴──────┴──────┴──────┴──────┴────┴──┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Full sandbox (konard/sandbox) │ +│ └─ COPY --from all language images │ +│ └─ + apt: .NET, R, C/C++, Assembly (system packages) │ +└─────────────────────────────────────────────────────────────┘ +``` + +Each language image is also available as a standalone Docker image +(e.g., `konard/sandbox-python`, `konard/sandbox-go`, etc.), each with +essentials pre-installed (JS, git, gh, glab, dev libraries). + +### Benefits + +1. **Configurable disk usage**: Users can choose only the languages they need +2. **Parallel CI/CD**: All language images are built in parallel +3. **Faster iteration**: Changes to one language only rebuild that image +4. **Efficient assembly**: Full sandbox uses `COPY --from` to merge pre-built files +5. **No dependency conflicts**: Each language builds in isolation on essentials +6. **Standalone scripts**: Each `install.sh` works directly on Ubuntu 24.04 via `curl | bash` + ## Design Decisions ### 1. Non-Root User diff --git a/Dockerfile b/Dockerfile index 574b887..c9e6cb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,147 @@ -FROM ubuntu:24.04 +# Full Sandbox environment Docker image +# Contains all language runtimes and development tools. +# This is the "full-sandbox" image (konard/sandbox or konard/sandbox-full). +# +# Architecture: +# essentials-sandbox (base for all language images) +# ├─ sandbox-python, sandbox-go, sandbox-rust, ... (built in parallel) +# └─ full-sandbox (merges all language images via COPY --from) +# +# Build from repository root: +# docker build -t sandbox . +# +# Build with specific images: +# docker build --build-arg ESSENTIALS_IMAGE=konard/sandbox-essentials:latest -t sandbox . +# +# For a lighter image with just essentials, see ubuntu/24.04/essentials-sandbox/Dockerfile. +# For just JavaScript, see ubuntu/24.04/js/Dockerfile. +# For individual language images, see ubuntu/24.04//Dockerfile. -# Sandbox environment Docker image -# Contains common language runtimes without any AI-specific tools -# This image is meant to be used as a base for other projects that need language runtimes. +# === Build arguments (all declared before first FROM for global scope) === +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +ARG PYTHON_IMAGE=konard/sandbox-python:latest +ARG GO_IMAGE=konard/sandbox-go:latest +ARG RUST_IMAGE=konard/sandbox-rust:latest +ARG JAVA_IMAGE=konard/sandbox-java:latest +ARG KOTLIN_IMAGE=konard/sandbox-kotlin:latest +ARG RUBY_IMAGE=konard/sandbox-ruby:latest +ARG PHP_IMAGE=konard/sandbox-php:latest +ARG PERL_IMAGE=konard/sandbox-perl:latest +ARG SWIFT_IMAGE=konard/sandbox-swift:latest +ARG LEAN_IMAGE=konard/sandbox-lean:latest +ARG ROCQ_IMAGE=konard/sandbox-rocq:latest -# Set non-interactive frontend for apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set working directory -WORKDIR /workspace +FROM ${PYTHON_IMAGE} AS python-stage +FROM ${GO_IMAGE} AS go-stage +FROM ${RUST_IMAGE} AS rust-stage +FROM ${JAVA_IMAGE} AS java-stage +FROM ${KOTLIN_IMAGE} AS kotlin-stage +FROM ${RUBY_IMAGE} AS ruby-stage +FROM ${PHP_IMAGE} AS php-stage +FROM ${PERL_IMAGE} AS perl-stage +FROM ${SWIFT_IMAGE} AS swift-stage +FROM ${LEAN_IMAGE} AS lean-stage +FROM ${ROCQ_IMAGE} AS rocq-stage -# Copy the installation script -COPY scripts/ubuntu-24-server-install.sh /tmp/ubuntu-24-server-install.sh +# === Final assembly image === +FROM ${ESSENTIALS_IMAGE} -# Make the script executable and run it -# Pass DOCKER_BUILD=1 environment variable to indicate Docker build environment -RUN chmod +x /tmp/ubuntu-24-server-install.sh && \ - DOCKER_BUILD=1 bash /tmp/ubuntu-24-server-install.sh && \ - rm -f /tmp/ubuntu-24-server-install.sh +USER root +ENV DEBIAN_FRONTEND=noninteractive +WORKDIR /workspace -# Copy entrypoint script for proper environment initialization +# Copy entrypoint script COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +# --- Install system-level packages (cannot be COPY'd from images) --- +RUN apt-get update -y && \ + apt-get install -y \ + dotnet-sdk-8.0 \ + r-base \ + cmake clang llvm lld \ + nasm \ + bubblewrap && \ + # FASM only available on x86_64 + if [ "$(uname -m)" = "x86_64" ]; then apt-get install -y fasm; fi && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# --- Prepare directories for COPY --from --- +RUN mkdir -p /home/linuxbrew/.linuxbrew && \ + chown -R sandbox:sandbox /home/linuxbrew + +# --- Copy user-home language runtimes from pre-built images --- + +# Python (pyenv) +COPY --from=python-stage --chown=sandbox:sandbox /home/sandbox/.pyenv /home/sandbox/.pyenv + +# Go +COPY --from=go-stage --chown=sandbox:sandbox /home/sandbox/.go /home/sandbox/.go + +# Rust (cargo + rustup) +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.cargo /home/sandbox/.cargo +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.rustup /home/sandbox/.rustup + +# Java (SDKMAN) +COPY --from=java-stage --chown=sandbox:sandbox /home/sandbox/.sdkman /home/sandbox/.sdkman + +# Kotlin (SDKMAN - merge Kotlin candidate into Java's SDKMAN) +COPY --from=kotlin-stage --chown=sandbox:sandbox /home/sandbox/.sdkman/candidates/kotlin /home/sandbox/.sdkman/candidates/kotlin + +# Ruby (rbenv) +COPY --from=ruby-stage --chown=sandbox:sandbox /home/sandbox/.rbenv /home/sandbox/.rbenv + +# PHP (Homebrew) +COPY --from=php-stage --chown=sandbox:sandbox /home/linuxbrew/.linuxbrew /home/linuxbrew/.linuxbrew + +# Perl (Perlbrew) +COPY --from=perl-stage --chown=sandbox:sandbox /home/sandbox/.perl5 /home/sandbox/.perl5 + +# Swift +COPY --from=swift-stage --chown=sandbox:sandbox /home/sandbox/.swift /home/sandbox/.swift + +# Lean (elan) +COPY --from=lean-stage --chown=sandbox:sandbox /home/sandbox/.elan /home/sandbox/.elan + +# Rocq/Coq (Opam) +COPY --from=rocq-stage --chown=sandbox:sandbox /home/sandbox/.opam /home/sandbox/.opam + +# --- Copy bashrc configurations from language stages --- +COPY --from=python-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-python +COPY --from=go-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-go +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-rust +COPY --from=java-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-java +COPY --from=kotlin-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-kotlin +COPY --from=ruby-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-ruby +COPY --from=php-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-php +COPY --from=perl-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-perl +COPY --from=swift-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-swift +COPY --from=lean-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-lean +COPY --from=rocq-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-rocq + +# Merge bashrc configurations: take the essentials bashrc as base, +# then append unique lines from each language stage +RUN cp /home/sandbox/.bashrc /tmp/.bashrc-base && \ + for lang_bashrc in /tmp/.bashrc-python /tmp/.bashrc-go /tmp/.bashrc-rust \ + /tmp/.bashrc-java /tmp/.bashrc-kotlin /tmp/.bashrc-ruby /tmp/.bashrc-php \ + /tmp/.bashrc-perl /tmp/.bashrc-swift /tmp/.bashrc-lean /tmp/.bashrc-rocq; do \ + if [ -f "$lang_bashrc" ]; then \ + while IFS= read -r line; do \ + if [ -n "$line" ] && ! grep -qxF "$line" /tmp/.bashrc-base 2>/dev/null; then \ + echo "$line" >> /tmp/.bashrc-base; \ + fi; \ + done < "$lang_bashrc"; \ + fi; \ + done && \ + cp /tmp/.bashrc-base /home/sandbox/.bashrc && \ + chown sandbox:sandbox /home/sandbox/.bashrc && \ + rm -f /tmp/.bashrc-* + # Switch to sandbox user USER sandbox - -# Set home directory WORKDIR /home/sandbox -# Set up basic environment variables (tools will be fully loaded by entrypoint) +# Environment variables for all tools ENV NVM_DIR="/home/sandbox/.nvm" ENV PYENV_ROOT="/home/sandbox/.pyenv" ENV BUN_INSTALL="/home/sandbox/.bun" @@ -41,20 +153,14 @@ ENV SDKMAN_DIR="/home/sandbox/.sdkman" ENV PERLBREW_ROOT="/home/sandbox/.perl5" ENV RBENV_ROOT="/home/sandbox/.rbenv" -# Set up PATH for tools that don't need special initialization -# Bun, Deno, Cargo, elan, rbenv, Swift, Homebrew work with just PATH -ENV PATH="/home/sandbox/.rbenv/bin:/home/sandbox/.rbenv/shims:/home/sandbox/.swift/usr/bin:/home/sandbox/.elan/bin:/home/sandbox/.opam/default/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/sbin:/home/sandbox/.cargo/bin:/home/sandbox/.deno/bin:/home/sandbox/.bun/bin:/home/sandbox/.go/bin:/home/sandbox/.go/path/bin:/home/linuxbrew/.linuxbrew/bin:${PATH}" +# PATH for tools that don't need special initialization +ENV PATH="/home/sandbox/.pyenv/bin:/home/sandbox/.pyenv/shims:/home/sandbox/.rbenv/bin:/home/sandbox/.rbenv/shims:/home/sandbox/.swift/usr/bin:/home/sandbox/.elan/bin:/home/sandbox/.opam/default/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/sbin:/home/sandbox/.cargo/bin:/home/sandbox/.deno/bin:/home/sandbox/.bun/bin:/home/sandbox/.go/bin:/home/sandbox/.go/path/bin:/home/linuxbrew/.linuxbrew/bin:${PATH}" # Opam environment variables for Rocq/Coq theorem prover ENV OPAM_SWITCH_PREFIX="/home/sandbox/.opam/default" ENV CAML_LD_LIBRARY_PATH="/home/sandbox/.opam/default/lib/stublibs:/home/sandbox/.opam/default/lib/ocaml/stublibs:/home/sandbox/.opam/default/lib/ocaml" ENV OCAML_TOPLEVEL_PATH="/home/sandbox/.opam/default/lib/toplevel" -# Use bash as default shell SHELL ["/bin/bash", "-c"] - -# Use entrypoint to initialize environment ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] - -# Set default command to bash CMD ["/bin/bash"] diff --git a/README.md b/README.md index 1fe8f11..ad8d769 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,94 @@ This sandbox provides a pre-configured development environment with common langu - **GitHub CLI (gh)** - **Homebrew** +## Modular Architecture + +The sandbox is split into modular components, allowing you to use only what you need: + +``` +JS sandbox (konard/sandbox-js) + └─ Essentials sandbox (konard/sandbox-essentials) + ├─ sandbox-python (built in parallel) + ├─ sandbox-go (built in parallel) + ├─ sandbox-rust (built in parallel) + ├─ sandbox-java (built in parallel) + ├─ sandbox-kotlin (built in parallel) + ├─ sandbox-ruby (built in parallel) + ├─ sandbox-php (built in parallel) + ├─ sandbox-perl (built in parallel) + ├─ sandbox-swift (built in parallel) + ├─ sandbox-lean (built in parallel) + └─ sandbox-rocq (built in parallel) + └─ Full sandbox (konard/sandbox) ← merges all via COPY --from +``` + +| Image | Description | Base Image | +|-------|-------------|------------| +| `konard/sandbox` | Full sandbox (all languages) | Assembled from all language images | +| `konard/sandbox-essentials` | Essentials (git identity tools) | Built on JS sandbox | +| `konard/sandbox-js` | JavaScript only | Ubuntu 24.04 | +| `konard/sandbox-python` | Python (pyenv) | Built on essentials | +| `konard/sandbox-go` | Go (latest stable) | Built on essentials | +| `konard/sandbox-rust` | Rust (rustup + cargo) | Built on essentials | +| `konard/sandbox-java` | Java 21 (SDKMAN + Temurin) | Built on essentials | +| `konard/sandbox-kotlin` | Kotlin (SDKMAN) | Built on essentials | +| `konard/sandbox-ruby` | Ruby (rbenv) | Built on essentials | +| `konard/sandbox-php` | PHP 8.3 (Homebrew) | Built on essentials | +| `konard/sandbox-perl` | Perl (Perlbrew) | Built on essentials | +| `konard/sandbox-swift` | Swift 6.x | Built on essentials | +| `konard/sandbox-lean` | Lean (elan) | Built on essentials | +| `konard/sandbox-rocq` | Rocq/Coq (Opam) | Built on essentials | + +### Per-Language Install Scripts & Dockerfiles + +Each language has its own standalone `install.sh` and `Dockerfile` under `ubuntu/24.04//`: + +| Language | Directory | Key Tools | +|----------|-----------|-----------| +| JavaScript/TypeScript | `ubuntu/24.04/js/` | NVM, Node.js, Bun, Deno, npm | +| Python | `ubuntu/24.04/python/` | Pyenv, latest stable Python | +| Go | `ubuntu/24.04/go/` | Latest stable Go | +| Rust | `ubuntu/24.04/rust/` | rustup, Cargo | +| Java | `ubuntu/24.04/java/` | SDKMAN, Eclipse Temurin 21 | +| Kotlin | `ubuntu/24.04/kotlin/` | SDKMAN, Kotlin | +| .NET | `ubuntu/24.04/dotnet/` | .NET SDK 8.0 | +| R | `ubuntu/24.04/r/` | R base | +| Ruby | `ubuntu/24.04/ruby/` | rbenv, latest Ruby 3.x | +| PHP | `ubuntu/24.04/php/` | Homebrew, PHP 8.3 | +| Perl | `ubuntu/24.04/perl/` | Perlbrew, latest Perl | +| Swift | `ubuntu/24.04/swift/` | Swift 6.x | +| Lean | `ubuntu/24.04/lean/` | elan, Lean prover | +| Rocq/Coq | `ubuntu/24.04/rocq/` | Opam, Rocq prover | +| C/C++ | `ubuntu/24.04/cpp/` | CMake, Clang, LLVM, LLD | +| Assembly | `ubuntu/24.04/assembly/` | NASM, FASM (x86_64) | + +Each install script can be run standalone on Ubuntu 24.04: + +```bash +# Install just Go on your Ubuntu 24.04 system +curl -fsSL https://raw.githubusercontent.com/link-foundation/sandbox/main/ubuntu/24.04/go/install.sh | bash +``` + ## Usage -### Pull the image +### Pull the full image ```bash docker pull ghcr.io/link-foundation/sandbox:latest ``` +### Pull the JS image + +```bash +docker pull ghcr.io/link-foundation/sandbox-js:latest +``` + +### Pull the essentials image + +```bash +docker pull ghcr.io/link-foundation/sandbox-essentials:latest +``` + ### Run interactively ```bash diff --git a/ubuntu/24.04/assembly/Dockerfile b/ubuntu/24.04/assembly/Dockerfile new file mode 100644 index 0000000..2fb235c --- /dev/null +++ b/ubuntu/24.04/assembly/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/assembly/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-assembly . + +# Assembly sandbox: NASM, FASM (x86_64 only), GNU Assembler, LLVM MC +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root + +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/assembly/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +USER sandbox +WORKDIR /home/sandbox + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/assembly/install.sh b/ubuntu/24.04/assembly/install.sh new file mode 100644 index 0000000..d904b1c --- /dev/null +++ b/ubuntu/24.04/assembly/install.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Assembly tools installation (NASM, FASM) +# Usage: curl -fsSL | bash OR bash install.sh +# Note: FASM is only available on x86_64 architecture + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_note() { echo "[i] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Assembly Tools" + +ARCH=$(uname -m) +if [ "$ARCH" = "x86_64" ]; then + maybe_sudo apt install -y nasm fasm + log_success "Assembly tools installed (NASM + FASM)" +else + maybe_sudo apt install -y nasm + log_success "Assembly tools installed (NASM only - FASM not available for $ARCH)" +fi + +log_success "Assembly tools installation complete" diff --git a/ubuntu/24.04/common.sh b/ubuntu/24.04/common.sh new file mode 100644 index 0000000..13c0856 --- /dev/null +++ b/ubuntu/24.04/common.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# Common functions and utilities shared across all sandbox install scripts +# Source this file at the top of each install.sh: +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# source "$SCRIPT_DIR/../common.sh" + +set -euo pipefail + +# Color codes for enhanced output (disabled in non-TTY) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + CYAN='\033[0;36m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + CYAN='' + NC='' +fi + +# Enhanced logging functions +log_info() { echo -e "${BLUE}[*]${NC} $1"; } +log_success() { echo -e "${GREEN}[✓]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[!]${NC} $1"; } +log_error() { echo -e "${RED}[✗]${NC} $1"; } +log_note() { echo -e "${CYAN}[i]${NC} $1"; } +log_step() { echo -e "\n${GREEN}==>${NC} ${BLUE}$1${NC}\n"; } + +# Verification helper +verify_command() { + local tool_name="$1" + local command_name="${2:-$1}" + local version_flag="${3:---version}" + + if command -v "$command_name" &>/dev/null; then + local version=$("$command_name" $version_flag 2>/dev/null | head -n1 || echo "installed") + log_success "$tool_name: $version" + return 0 + else + log_warning "$tool_name: not found in PATH" + return 1 + fi +} + +# Check if a command exists (silent) +command_exists() { + command -v "$1" &>/dev/null +} + +# Run command with sudo only if not root and sudo is available +maybe_sudo() { + if [ "$EUID" -eq 0 ]; then + "$@" + elif command_exists sudo; then + sudo "$@" + else + "$@" + fi +} + +# Safe apt update +apt_update_safe() { + log_info "Updating apt sources..." + for f in /etc/apt/sources.list.d/*.list; do + if [ -f "$f" ] && ! grep -Eq "^deb " "$f"; then + log_warning "Removing malformed apt source: $f" + maybe_sudo rm -f "$f" + fi + done + maybe_sudo apt update -y || true +} + +# Cleanup apt cache +apt_cleanup() { + log_info "Cleaning up apt cache and temporary files..." + maybe_sudo apt-get clean + maybe_sudo apt-get autoclean + maybe_sudo apt-get autoremove -y + maybe_sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + log_success "Cleanup completed" +} + +# Cleanup duplicate APT sources +cleanup_duplicate_apt_sources() { + log_info "Checking for duplicate APT sources..." + local duplicates_found=false + + if [ -f /etc/apt/sources.list.d/microsoft-edge.list ] && \ + [ -f /etc/apt/sources.list.d/microsoft-edge-stable.list ]; then + log_info "Found duplicate Microsoft Edge APT sources" + maybe_sudo rm -f /etc/apt/sources.list.d/microsoft-edge.list + duplicates_found=true + fi + + if [ -f /etc/apt/sources.list.d/google-chrome.list ] && \ + [ -f /etc/apt/sources.list.d/google-chrome-stable.list ]; then + log_info "Found duplicate Google Chrome APT sources" + maybe_sudo rm -f /etc/apt/sources.list.d/google-chrome-stable.list + duplicates_found=true + fi + + if [ "$duplicates_found" = true ]; then + log_success "Duplicate APT sources cleaned up" + else + log_success "No duplicate APT sources found" + fi +} + +# Create sandbox user if missing +ensure_sandbox_user() { + if id "sandbox" &>/dev/null; then + log_info "sandbox user already exists." + else + log_info "Creating sandbox user..." + useradd -m -s /bin/bash sandbox 2>/dev/null || { + log_warning "User creation with useradd failed, trying adduser..." + adduser --disabled-password --gecos "" sandbox + } + passwd -d sandbox 2>/dev/null || log_note "Could not remove password requirement" + usermod -aG sudo sandbox 2>/dev/null || log_note "Could not add to sudo group" + log_success "sandbox user created and configured" + fi +} + +# Detect Docker environment +is_docker_build() { + if [ "${DOCKER_BUILD:-}" = "1" ]; then + return 0 + elif [ -f /.dockerenv ]; then + return 0 + elif grep -qE 'docker|buildkit|containerd' /proc/1/cgroup 2>/dev/null; then + return 0 + fi + return 1 +} diff --git a/ubuntu/24.04/cpp/Dockerfile b/ubuntu/24.04/cpp/Dockerfile new file mode 100644 index 0000000..c0deace --- /dev/null +++ b/ubuntu/24.04/cpp/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/cpp/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-cpp . + +# C/C++ sandbox: build-essential, CMake, Clang, LLVM, LLD +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root + +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/cpp/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +USER sandbox +WORKDIR /home/sandbox + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/cpp/install.sh b/ubuntu/24.04/cpp/install.sh new file mode 100644 index 0000000..4d1ae61 --- /dev/null +++ b/ubuntu/24.04/cpp/install.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# C/C++ development tools installation (CMake, Clang, LLVM, LLD) +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing C/C++ Development Tools" + +log_info "Installing build-essential, CMake, Clang/LLVM, LLD..." +maybe_sudo apt install -y build-essential cmake clang llvm lld +log_success "C/C++ development tools installed" + +log_success "C/C++ tools installation complete" diff --git a/ubuntu/24.04/dotnet/Dockerfile b/ubuntu/24.04/dotnet/Dockerfile new file mode 100644 index 0000000..c32185f --- /dev/null +++ b/ubuntu/24.04/dotnet/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/dotnet/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-dotnet . + +# .NET sandbox: .NET SDK 8.0 +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root + +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/dotnet/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +USER sandbox +WORKDIR /home/sandbox + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/dotnet/install.sh b/ubuntu/24.04/dotnet/install.sh new file mode 100644 index 0000000..3c0c224 --- /dev/null +++ b/ubuntu/24.04/dotnet/install.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# .NET SDK 8.0 installation +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing .NET SDK 8.0" + +if ! command_exists dotnet; then + log_info "Installing .NET SDK 8.0..." + maybe_sudo apt install -y dotnet-sdk-8.0 + log_success ".NET SDK 8.0 installed" +else + log_info ".NET SDK already installed." +fi + +log_success ".NET installation complete" diff --git a/ubuntu/24.04/essentials-sandbox/Dockerfile b/ubuntu/24.04/essentials-sandbox/Dockerfile new file mode 100644 index 0000000..45fc851 --- /dev/null +++ b/ubuntu/24.04/essentials-sandbox/Dockerfile @@ -0,0 +1,37 @@ +ARG JS_IMAGE=konard/sandbox-js:latest +FROM ${JS_IMAGE} + +# Essentials Sandbox Docker image +# Built on top of the JS sandbox, adds git identity tools (gh, glab) +# Published as: konard/sandbox-essentials +# +# Build from repository root: +# docker build -f ubuntu/24.04/essentials-sandbox/Dockerfile -t sandbox-essentials . +# +# Build with specific JS image: +# docker build -f ubuntu/24.04/essentials-sandbox/Dockerfile \ +# --build-arg JS_IMAGE=konard/sandbox-js:latest -t sandbox-essentials . + +USER root + +ENV DEBIAN_FRONTEND=noninteractive + +# Copy install script +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/essentials-sandbox/install.sh /tmp/install.sh +COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh + +RUN chmod +x /tmp/install.sh /tmp/common.sh /usr/local/bin/entrypoint.sh && \ + DOCKER_BUILD=1 bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +USER sandbox +WORKDIR /home/sandbox + +# Environment variables inherited from JS sandbox base image +# Additional ones for essentials are not needed (gh, glab are system-wide) + +SHELL ["/bin/bash", "-c"] + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/essentials-sandbox/install.sh b/ubuntu/24.04/essentials-sandbox/install.sh new file mode 100644 index 0000000..22d93d5 --- /dev/null +++ b/ubuntu/24.04/essentials-sandbox/install.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Essentials Sandbox Installation Script +# Installs tooling required for gh-setup-git-identity and glab-setup-git-identity +# on top of an existing JS sandbox (Node.js, Bun, Deno already available). +# +# Components added: system essentials, GitHub CLI, GitLab CLI, git identity tools +# +# This is the layer between JS sandbox and full-sandbox. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +else + # Inline fallback logging + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_error() { echo "[✗] $1"; } + log_note() { echo "[i] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Essentials Sandbox (on top of JS sandbox)" + +# --- Pre-flight Checks --- +log_step "Running pre-flight checks" + +if [ "$EUID" -ne 0 ] && ! sudo -n true 2>/dev/null; then + log_error "This script requires sudo access." + exit 1 +fi + +if [ -f /etc/os-release ]; then + source /etc/os-release + if [[ "${ID:-}" == "ubuntu" ]]; then + log_success "Ubuntu ${VERSION_ID:-unknown} detected" + else + log_warning "This script is designed for Ubuntu. Detected: ${ID:-unknown}" + fi +fi + +log_success "Pre-flight checks passed" + +# --- Ensure sandbox user exists --- +log_step "Setting up sandbox user" +if id "sandbox" &>/dev/null; then + log_info "sandbox user already exists." +else + log_info "Creating sandbox user..." + useradd -m -s /bin/bash sandbox 2>/dev/null || adduser --disabled-password --gecos "" sandbox + passwd -d sandbox 2>/dev/null || true + usermod -aG sudo sandbox 2>/dev/null || true + log_success "sandbox user created" +fi + +# --- System prerequisites --- +log_step "Installing system prerequisites" + +# Clean up duplicate sources +for pair in "microsoft-edge:microsoft-edge-stable" "google-chrome:google-chrome-stable"; do + f1="/etc/apt/sources.list.d/${pair%%:*}.list" + f2="/etc/apt/sources.list.d/${pair##*:}.list" + if [ -f "$f1" ] && [ -f "$f2" ]; then + maybe_sudo rm -f "$f1" + fi +done + +maybe_sudo apt update -y || true + +# Core system tools +maybe_sudo apt install -y \ + wget curl unzip zip git sudo ca-certificates gnupg \ + build-essential expect screen + +# Common development libraries used by multiple language runtimes +# (Python, Ruby, Rust, Go, etc. all benefit from these) +maybe_sudo apt install -y \ + libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \ + libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev \ + libffi-dev liblzma-dev libyaml-dev + +log_success "System prerequisites installed" + +# --- GitHub CLI --- +log_step "Installing GitHub CLI" +if ! command_exists gh; then + maybe_sudo mkdir -p -m 755 /etc/apt/keyrings + out=$(mktemp) + wget -nv -O"$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg + cat "$out" | maybe_sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null + maybe_sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg + rm -f "$out" + + maybe_sudo mkdir -p -m 755 /etc/apt/sources.list.d + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + | maybe_sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + + maybe_sudo apt update -y + maybe_sudo apt install -y gh + log_success "GitHub CLI installed" +else + log_success "GitHub CLI already installed" +fi + +# --- GitLab CLI --- +log_step "Installing GitLab CLI" +if ! command_exists glab; then + maybe_sudo apt install -y glab + log_success "GitLab CLI installed" +else + log_success "GitLab CLI already installed" +fi + +# --- Install git identity tools as sandbox user (using Bun from JS sandbox) --- +log_step "Installing git identity tools" + +cat > /tmp/essentials-identity-setup.sh <<'EOF_IDENTITY' +#!/usr/bin/env bash +set -euo pipefail + +log_info() { echo "[*] $1"; } +log_success() { echo "[✓] $1"; } +command_exists() { command -v "$1" &>/dev/null; } + +# Load Bun from JS sandbox +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" + +# gh-setup-git-identity +if command_exists bun; then + if ! command_exists gh-setup-git-identity; then + log_info "Installing gh-setup-git-identity..." + bun install -g gh-setup-git-identity + fi +fi + +# glab-setup-git-identity +if command_exists bun; then + if ! command_exists glab-setup-git-identity; then + log_info "Installing glab-setup-git-identity..." + bun install -g glab-setup-git-identity + fi +fi + +# Git setup if gh is authenticated +if gh auth status &>/dev/null; then + log_info "Configuring Git with GitHub identity..." + git config --global user.name "$(gh api user --jq .login)" + git config --global user.email "$(gh api user/emails --jq '.[] | select(.primary==true).email')" + gh auth setup-git +fi + +log_success "Essentials identity tools setup complete" +EOF_IDENTITY + +chmod +x /tmp/essentials-identity-setup.sh +if [ "$EUID" -eq 0 ]; then + su - sandbox -c "bash /tmp/essentials-identity-setup.sh" +else + sudo -i -u sandbox bash /tmp/essentials-identity-setup.sh +fi +rm -f /tmp/essentials-identity-setup.sh + +# --- Cleanup --- +log_step "Cleaning up" +maybe_sudo apt-get clean +maybe_sudo apt-get autoclean +maybe_sudo apt-get autoremove -y +maybe_sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +log_step "Essentials Sandbox setup complete!" +log_success "Added on top of JS sandbox: git, gh, glab, gh-setup-git-identity, glab-setup-git-identity" diff --git a/ubuntu/24.04/full-sandbox/Dockerfile b/ubuntu/24.04/full-sandbox/Dockerfile new file mode 100644 index 0000000..ad55af4 --- /dev/null +++ b/ubuntu/24.04/full-sandbox/Dockerfile @@ -0,0 +1,172 @@ +# Full Sandbox Docker image - Multi-stage assembly +# Copies pre-built language runtimes from individual language images +# Published as: konard/sandbox (or konard/sandbox-full) +# +# Architecture: +# essentials-sandbox (base for all language images) +# ├─ sandbox-python (built in parallel) +# ├─ sandbox-go (built in parallel) +# ├─ sandbox-rust (built in parallel) +# ├─ sandbox-java (built in parallel) +# ├─ sandbox-kotlin (built in parallel) +# ├─ sandbox-ruby (built in parallel) +# ├─ sandbox-php (built in parallel) +# ├─ sandbox-perl (built in parallel) +# ├─ sandbox-swift (built in parallel) +# ├─ sandbox-lean (built in parallel) +# ├─ sandbox-rocq (built in parallel) +# └─ full-sandbox (merges all + adds dotnet, r, cpp, assembly via apt) +# +# Build from repository root: +# docker build -f ubuntu/24.04/full-sandbox/Dockerfile -t sandbox-full . + +# === Build arguments (all declared before first FROM for global scope) === +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +ARG PYTHON_IMAGE=konard/sandbox-python:latest +ARG GO_IMAGE=konard/sandbox-go:latest +ARG RUST_IMAGE=konard/sandbox-rust:latest +ARG JAVA_IMAGE=konard/sandbox-java:latest +ARG KOTLIN_IMAGE=konard/sandbox-kotlin:latest +ARG RUBY_IMAGE=konard/sandbox-ruby:latest +ARG PHP_IMAGE=konard/sandbox-php:latest +ARG PERL_IMAGE=konard/sandbox-perl:latest +ARG SWIFT_IMAGE=konard/sandbox-swift:latest +ARG LEAN_IMAGE=konard/sandbox-lean:latest +ARG ROCQ_IMAGE=konard/sandbox-rocq:latest + +# === Language image stages (for COPY --from) === +FROM ${PYTHON_IMAGE} AS python-stage +FROM ${GO_IMAGE} AS go-stage +FROM ${RUST_IMAGE} AS rust-stage +FROM ${JAVA_IMAGE} AS java-stage +FROM ${KOTLIN_IMAGE} AS kotlin-stage +FROM ${RUBY_IMAGE} AS ruby-stage +FROM ${PHP_IMAGE} AS php-stage +FROM ${PERL_IMAGE} AS perl-stage +FROM ${SWIFT_IMAGE} AS swift-stage +FROM ${LEAN_IMAGE} AS lean-stage +FROM ${ROCQ_IMAGE} AS rocq-stage + +# === Final assembly image === +FROM ${ESSENTIALS_IMAGE} + +USER root +ENV DEBIAN_FRONTEND=noninteractive + +# Copy entrypoint script +COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# --- Install system-level packages (cannot be COPY'd from images) --- +RUN apt-get update -y && \ + apt-get install -y \ + dotnet-sdk-8.0 \ + r-base \ + cmake clang llvm lld \ + nasm \ + bubblewrap && \ + # FASM only available on x86_64 + if [ "$(uname -m)" = "x86_64" ]; then apt-get install -y fasm; fi && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# --- Prepare directories for COPY --from --- +RUN mkdir -p /home/linuxbrew/.linuxbrew && \ + chown -R sandbox:sandbox /home/linuxbrew + +# --- Copy user-home language runtimes from pre-built images --- + +# Python (pyenv) +COPY --from=python-stage --chown=sandbox:sandbox /home/sandbox/.pyenv /home/sandbox/.pyenv + +# Go +COPY --from=go-stage --chown=sandbox:sandbox /home/sandbox/.go /home/sandbox/.go + +# Rust (cargo + rustup) +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.cargo /home/sandbox/.cargo +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.rustup /home/sandbox/.rustup + +# Java (SDKMAN) +COPY --from=java-stage --chown=sandbox:sandbox /home/sandbox/.sdkman /home/sandbox/.sdkman + +# Kotlin (SDKMAN - merge with Java's SDKMAN) +# Kotlin stage has its own SDKMAN with Kotlin installed +# We copy only the Kotlin candidate from the kotlin stage +COPY --from=kotlin-stage --chown=sandbox:sandbox /home/sandbox/.sdkman/candidates/kotlin /home/sandbox/.sdkman/candidates/kotlin + +# Ruby (rbenv) +COPY --from=ruby-stage --chown=sandbox:sandbox /home/sandbox/.rbenv /home/sandbox/.rbenv + +# PHP (Homebrew) +COPY --from=php-stage --chown=sandbox:sandbox /home/linuxbrew/.linuxbrew /home/linuxbrew/.linuxbrew + +# Perl (Perlbrew) +COPY --from=perl-stage --chown=sandbox:sandbox /home/sandbox/.perl5 /home/sandbox/.perl5 + +# Swift +COPY --from=swift-stage --chown=sandbox:sandbox /home/sandbox/.swift /home/sandbox/.swift + +# Lean (elan) +COPY --from=lean-stage --chown=sandbox:sandbox /home/sandbox/.elan /home/sandbox/.elan + +# Rocq/Coq (Opam) +COPY --from=rocq-stage --chown=sandbox:sandbox /home/sandbox/.opam /home/sandbox/.opam + +# --- Copy bashrc configurations from language stages --- +# We need the bashrc entries for environment initialization +COPY --from=python-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-python +COPY --from=go-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-go +COPY --from=rust-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-rust +COPY --from=java-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-java +COPY --from=kotlin-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-kotlin +COPY --from=ruby-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-ruby +COPY --from=php-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-php +COPY --from=perl-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-perl +COPY --from=swift-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-swift +COPY --from=lean-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-lean +COPY --from=rocq-stage --chown=sandbox:sandbox /home/sandbox/.bashrc /tmp/.bashrc-rocq + +# Merge bashrc configurations: take the essentials bashrc as base, +# then append unique lines from each language stage +RUN cp /home/sandbox/.bashrc /tmp/.bashrc-base && \ + for lang_bashrc in /tmp/.bashrc-python /tmp/.bashrc-go /tmp/.bashrc-rust \ + /tmp/.bashrc-java /tmp/.bashrc-kotlin /tmp/.bashrc-ruby /tmp/.bashrc-php \ + /tmp/.bashrc-perl /tmp/.bashrc-swift /tmp/.bashrc-lean /tmp/.bashrc-rocq; do \ + if [ -f "$lang_bashrc" ]; then \ + while IFS= read -r line; do \ + if [ -n "$line" ] && ! grep -qxF "$line" /tmp/.bashrc-base 2>/dev/null; then \ + echo "$line" >> /tmp/.bashrc-base; \ + fi; \ + done < "$lang_bashrc"; \ + fi; \ + done && \ + cp /tmp/.bashrc-base /home/sandbox/.bashrc && \ + chown sandbox:sandbox /home/sandbox/.bashrc && \ + rm -f /tmp/.bashrc-* + +USER sandbox +WORKDIR /home/sandbox + +# Environment variables for all tools +ENV NVM_DIR="/home/sandbox/.nvm" +ENV PYENV_ROOT="/home/sandbox/.pyenv" +ENV BUN_INSTALL="/home/sandbox/.bun" +ENV DENO_INSTALL="/home/sandbox/.deno" +ENV CARGO_HOME="/home/sandbox/.cargo" +ENV GOROOT="/home/sandbox/.go" +ENV GOPATH="/home/sandbox/.go/path" +ENV SDKMAN_DIR="/home/sandbox/.sdkman" +ENV PERLBREW_ROOT="/home/sandbox/.perl5" +ENV RBENV_ROOT="/home/sandbox/.rbenv" + +# PATH for tools that don't need special initialization +ENV PATH="/home/sandbox/.pyenv/bin:/home/sandbox/.pyenv/shims:/home/sandbox/.rbenv/bin:/home/sandbox/.rbenv/shims:/home/sandbox/.swift/usr/bin:/home/sandbox/.elan/bin:/home/sandbox/.opam/default/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/sbin:/home/sandbox/.cargo/bin:/home/sandbox/.deno/bin:/home/sandbox/.bun/bin:/home/sandbox/.go/bin:/home/sandbox/.go/path/bin:/home/linuxbrew/.linuxbrew/bin:${PATH}" + +# Opam environment variables for Rocq/Coq theorem prover +ENV OPAM_SWITCH_PREFIX="/home/sandbox/.opam/default" +ENV CAML_LD_LIBRARY_PATH="/home/sandbox/.opam/default/lib/stublibs:/home/sandbox/.opam/default/lib/ocaml/stublibs:/home/sandbox/.opam/default/lib/ocaml" +ENV OCAML_TOPLEVEL_PATH="/home/sandbox/.opam/default/lib/toplevel" + +SHELL ["/bin/bash", "-c"] + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/full-sandbox/install.sh b/ubuntu/24.04/full-sandbox/install.sh new file mode 100644 index 0000000..b8e71ca --- /dev/null +++ b/ubuntu/24.04/full-sandbox/install.sh @@ -0,0 +1,464 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Full Sandbox Installation Script +# Installs all additional language runtimes and development tools +# on top of the essentials-sandbox (which already includes JS + git identity tools). +# +# Architecture: +# JS sandbox → essentials-sandbox → full-sandbox (this script) +# +# Each language installer is a standalone script under ubuntu/24.04//install.sh. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +else + # Inline fallback logging + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_error() { echo "[✗] $1"; } + log_note() { echo "[i] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Full Sandbox (on top of essentials)" + +# --- Install additional system packages --- +log_step "Installing additional system packages" + +maybe_sudo apt update -y || true + +# .NET SDK +log_info "Installing .NET SDK 8.0..." +maybe_sudo apt install -y dotnet-sdk-8.0 +log_success ".NET SDK installed" + +# C/C++ tools +log_info "Installing C/C++ development tools..." +maybe_sudo apt install -y cmake clang llvm lld +log_success "C/C++ tools installed" + +# Assembly tools +log_info "Installing Assembly tools..." +ARCH=$(uname -m) +if [ "$ARCH" = "x86_64" ]; then + maybe_sudo apt install -y nasm fasm + log_success "Assembly tools installed (NASM + FASM)" +else + maybe_sudo apt install -y nasm + log_success "Assembly tools installed (NASM only)" +fi + +# R language +log_info "Installing R statistical language..." +maybe_sudo apt install -y r-base +log_success "R language installed" + +# Note: Common build dependencies (build-essential, libssl-dev, zlib1g-dev, +# libyaml-dev, etc.) are already installed in the essentials-sandbox layer. + +# Bubblewrap (needed by Rocq/Opam) +log_info "Installing bubblewrap..." +maybe_sudo apt install -y bubblewrap +log_success "Bubblewrap installed" + +# --- Prepare Homebrew directory --- +log_step "Preparing Homebrew directory" +if [ ! -d /home/linuxbrew/.linuxbrew ]; then + maybe_sudo mkdir -p /home/linuxbrew/.linuxbrew + if id "sandbox" &>/dev/null; then + maybe_sudo chown -R sandbox:sandbox /home/linuxbrew + fi +else + if id "sandbox" &>/dev/null; then + maybe_sudo chown -R sandbox:sandbox /home/linuxbrew + fi +fi + +# --- Install all language runtimes as sandbox user --- +log_step "Installing language runtimes as sandbox user" + +cat > /tmp/full-sandbox-user-setup.sh <<'EOF_FULL_SETUP' +#!/usr/bin/env bash +set -euo pipefail + +log_info() { echo "[*] $1"; } +log_success() { echo "[✓] $1"; } +log_warning() { echo "[!] $1"; } +log_note() { echo "[i] $1"; } +log_step() { echo "==> $1"; } +command_exists() { command -v "$1" &>/dev/null; } + +# Ensure JS tools are available (installed by essentials/JS sandbox) +export BUN_INSTALL="$HOME/.bun" +export DENO_INSTALL="$HOME/.deno" +export NVM_DIR="$HOME/.nvm" +export PATH="$BUN_INSTALL/bin:$DENO_INSTALL/bin:$PATH" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + +# --- Python (Pyenv) --- +log_step "Installing Python" +if [ ! -d "$HOME/.pyenv" ]; then + curl https://pyenv.run | bash + if ! grep -q 'pyenv init' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Pyenv configuration' + echo 'export PYENV_ROOT="$HOME/.pyenv"' + echo 'export PATH="$PYENV_ROOT/bin:$PATH"' + echo 'eval "$(pyenv init --path)"' + echo 'eval "$(pyenv init -)"' + } >> "$HOME/.bashrc" + fi +fi + +export PYENV_ROOT="$HOME/.pyenv" +export PATH="$PYENV_ROOT/bin:$PATH" +if command -v pyenv >/dev/null 2>&1; then + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + LATEST_PYTHON=$(pyenv install --list | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]') + if [ -n "$LATEST_PYTHON" ]; then + if ! pyenv versions --bare | grep -q "^${LATEST_PYTHON}$"; then + pyenv install "$LATEST_PYTHON" + fi + pyenv global "$LATEST_PYTHON" + fi +fi + +# --- Go --- +log_step "Installing Go" +if [ ! -d "$HOME/.go" ] && [ ! -d "/usr/local/go" ]; then + ARCH=$(uname -m) + case "$ARCH" in + x86_64) GO_ARCH="amd64" ;; + aarch64) GO_ARCH="arm64" ;; + *) GO_ARCH="" ;; + esac + if [ -n "$GO_ARCH" ]; then + GO_VERSION=$(curl -sL 'https://go.dev/VERSION?m=text' | head -n1) + if [ -n "$GO_VERSION" ]; then + TEMP_DIR=$(mktemp -d) + curl -sL "https://go.dev/dl/${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o "$TEMP_DIR/go.tar.gz" + mkdir -p "$HOME/.go" + tar -xzf "$TEMP_DIR/go.tar.gz" -C "$HOME/.go" --strip-components=1 + rm -rf "$TEMP_DIR" + if ! grep -q 'GOROOT.*\.go' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Go configuration' + echo 'export GOROOT="$HOME/.go"' + echo 'export GOPATH="$HOME/.go/path"' + echo 'export PATH="$GOROOT/bin:$GOPATH/bin:$PATH"' + } >> "$HOME/.bashrc" + fi + export GOROOT="$HOME/.go" + export GOPATH="$HOME/.go/path" + export PATH="$GOROOT/bin:$GOPATH/bin:$PATH" + mkdir -p "$GOPATH" + fi + fi +fi + +# --- Rust --- +log_step "Installing Rust" +if [ ! -d "$HOME/.cargo" ]; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + [ -f "$HOME/.cargo/env" ] && \. "$HOME/.cargo/env" +fi + +# --- Java (SDKMAN) --- +log_step "Installing Java" +if [ ! -d "$HOME/.sdkman" ]; then + curl -s "https://get.sdkman.io?rcupdate=false&ci=true" | bash + if ! grep -q 'sdkman-init.sh' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# SDKMAN configuration' + echo 'export SDKMAN_DIR="$HOME/.sdkman"' + echo '[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"' + } >> "$HOME/.bashrc" + fi +fi + +export SDKMAN_DIR="$HOME/.sdkman" +if [ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]; then + set +u + source "$SDKMAN_DIR/bin/sdkman-init.sh" + set -u + + if ! sdk list java 2>/dev/null | grep -q "21.*tem.*installed"; then + set +u + sdk install java 21-tem < /dev/null || sdk install java 21-open < /dev/null || true + set -u + fi +fi + +# --- Kotlin (SDKMAN) --- +log_step "Installing Kotlin" +export SDKMAN_DIR="$HOME/.sdkman" +if [ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]; then + set +u + source "$SDKMAN_DIR/bin/sdkman-init.sh" + set -u + if ! command_exists kotlin; then + set +u + sdk install kotlin < /dev/null || true + set -u + fi +fi + +# --- Lean (elan) --- +log_step "Installing Lean" +if [ ! -d "$HOME/.elan" ]; then + curl https://elan.lean-lang.org/elan-init.sh -sSf | sh -s -- -y --default-toolchain stable + [ -f "$HOME/.elan/env" ] && \. "$HOME/.elan/env" + if ! grep -q 'elan' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Lean (elan) configuration' + echo 'export PATH="$HOME/.elan/bin:$PATH"' + } >> "$HOME/.bashrc" + fi +fi + +# --- Rocq/Coq (Opam) --- +log_step "Installing Rocq/Coq" +if ! command_exists opam; then + bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh) --no-backup" <<< "y" || { + sudo apt install -y opam || true + } +fi + +if command_exists opam; then + if [ ! -d "$HOME/.opam" ]; then + opam init --disable-sandboxing --auto-setup -y || true + fi + eval "$(opam env --switch=default 2>/dev/null)" || true + + ROCQ_ACCESSIBLE=false + if command -v rocq &>/dev/null && rocq -v &>/dev/null; then ROCQ_ACCESSIBLE=true; fi + if command -v rocqc &>/dev/null; then ROCQ_ACCESSIBLE=true; fi + if command -v coqc &>/dev/null; then ROCQ_ACCESSIBLE=true; fi + + if [ "$ROCQ_ACCESSIBLE" = false ]; then + opam repo add rocq-released https://rocq-prover.org/opam/released 2>/dev/null || true + opam update 2>/dev/null || true + opam pin add rocq-prover --yes 2>/dev/null || opam install rocq-prover -y 2>/dev/null || opam install coq -y || true + eval "$(opam env --switch=default 2>/dev/null)" || true + fi + + if ! grep -q 'opam env' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Opam (OCaml/Rocq) configuration' + echo 'test -r $HOME/.opam/opam-init/init.sh && . $HOME/.opam/opam-init/init.sh > /dev/null 2> /dev/null || true' + } >> "$HOME/.bashrc" + fi +fi + +# --- Homebrew + PHP --- +log_step "Installing Homebrew + PHP" +if ! command_exists brew; then + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 2>&1 || true + + if [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + elif [[ -x "$HOME/.linuxbrew/bin/brew" ]]; then + eval "$("$HOME/.linuxbrew/bin/brew" shellenv)" + fi + + BREW_PREFIX=$(brew --prefix 2>/dev/null || echo "/home/linuxbrew/.linuxbrew") + if ! grep -q "brew shellenv" "$HOME/.profile" 2>/dev/null; then + echo "eval \"\$($BREW_PREFIX/bin/brew shellenv)\"" >> "$HOME/.profile" + fi + if ! grep -q "brew shellenv" "$HOME/.bashrc" 2>/dev/null; then + echo "eval \"\$($BREW_PREFIX/bin/brew shellenv)\"" >> "$HOME/.bashrc" + fi +else + eval "$(brew shellenv 2>/dev/null)" || true +fi + +if command_exists brew; then + if ! brew list --formula 2>/dev/null | grep -q "^php@"; then + if ! brew tap | grep -q "shivammathur/php"; then + brew tap shivammathur/php || true + fi + if brew tap | grep -q "shivammathur/php"; then + export HOMEBREW_NO_ANALYTICS=1 + export HOMEBREW_NO_AUTO_UPDATE=1 + brew install shivammathur/php/php@8.3 || true + if brew list --formula 2>/dev/null | grep -q "^php@8.3$"; then + brew link --overwrite --force shivammathur/php/php@8.3 2>&1 | grep -v "Warning" || true + BREW_PREFIX=$(brew --prefix 2>/dev/null || echo "") + if [[ -n "$BREW_PREFIX" && -d "$BREW_PREFIX/opt/php@8.3" ]]; then + export PATH="$BREW_PREFIX/opt/php@8.3/bin:$BREW_PREFIX/opt/php@8.3/sbin:$PATH" + if ! grep -q "php@8.3/bin" "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" << 'PHP_PATH_EOF' + +# PHP 8.3 PATH configuration +export PATH="$(brew --prefix)/opt/php@8.3/bin:$(brew --prefix)/opt/php@8.3/sbin:$PATH" +PHP_PATH_EOF + fi + fi + fi + fi + fi +fi + +# --- Perl (Perlbrew) --- +log_step "Installing Perl" +if [ ! -d "$HOME/.perl5" ]; then + export PERLBREW_ROOT="$HOME/.perl5" + curl -L https://install.perlbrew.pl | bash + + if ! grep -q 'perlbrew' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Perlbrew configuration' + echo 'if [ -n "$PS1" ]; then' + echo ' export PERLBREW_ROOT="$HOME/.perl5"' + echo ' [ -f "$PERLBREW_ROOT/etc/bashrc" ] && source "$PERLBREW_ROOT/etc/bashrc"' + echo 'fi' + } >> "$HOME/.bashrc" + fi + + if [ -f "$PERLBREW_ROOT/etc/bashrc" ]; then + sed -i 's/\$1/${1:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + sed -i 's/\$PERLBREW_LIB/${PERLBREW_LIB:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + sed -i 's/\$outsep/${outsep:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + + set +u + source "$PERLBREW_ROOT/etc/bashrc" + set -u + + PERLBREW_OUTPUT=$(perlbrew available 2>&1 || true) + LATEST_PERL=$(echo "$PERLBREW_OUTPUT" | grep -oE 'perl-5\.[0-9]+\.[0-9]+' | head -1 || true) + if [ -n "$LATEST_PERL" ]; then + if ! perlbrew list | grep -q "$LATEST_PERL"; then + perlbrew install "$LATEST_PERL" --notest || true + fi + if perlbrew list | grep -q "$LATEST_PERL"; then + perlbrew switch "$LATEST_PERL" + fi + fi + fi +fi + +# --- Ruby (rbenv) --- +log_step "Installing Ruby" +if [ ! -d "$HOME/.rbenv" ]; then + git clone https://github.com/rbenv/rbenv.git "$HOME/.rbenv" + mkdir -p "$HOME/.rbenv/plugins" + git clone https://github.com/rbenv/ruby-build.git "$HOME/.rbenv/plugins/ruby-build" + + if ! grep -q 'rbenv init' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# rbenv configuration' + echo 'export PATH="$HOME/.rbenv/bin:$PATH"' + echo 'eval "$(rbenv init - bash)"' + } >> "$HOME/.bashrc" + fi + + export PATH="$HOME/.rbenv/bin:$PATH" + eval "$(rbenv init - bash)" + + LATEST_RUBY=$(rbenv install -l 2>/dev/null | grep -E '^\s*3\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]') + if [ -n "$LATEST_RUBY" ]; then + if ! rbenv versions | grep -q "$LATEST_RUBY"; then + rbenv install "$LATEST_RUBY" + fi + rbenv global "$LATEST_RUBY" + fi +fi + +# --- Swift --- +log_step "Installing Swift" +if ! command_exists swift; then + ARCH=$(uname -m) + case "$ARCH" in + x86_64) SWIFT_DIR="ubuntu2404"; SWIFT_FILE_SUFFIX="ubuntu24.04" ;; + aarch64) SWIFT_DIR="ubuntu2404-aarch64"; SWIFT_FILE_SUFFIX="ubuntu24.04-aarch64" ;; + *) SWIFT_DIR=""; SWIFT_FILE_SUFFIX="" ;; + esac + + if [ -n "$SWIFT_DIR" ]; then + SWIFT_VERSION="6.0.3" + SWIFT_RELEASE="RELEASE" + SWIFT_PACKAGE="swift-${SWIFT_VERSION}-${SWIFT_RELEASE}-${SWIFT_FILE_SUFFIX}" + SWIFT_URL="https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_DIR}/swift-${SWIFT_VERSION}-${SWIFT_RELEASE}/${SWIFT_PACKAGE}.tar.gz" + + TEMP_DIR=$(mktemp -d) + if curl -fsSL "$SWIFT_URL" -o "$TEMP_DIR/swift.tar.gz"; then + mkdir -p "$HOME/.swift" + tar -xzf "$TEMP_DIR/swift.tar.gz" -C "$TEMP_DIR" + cp -r "$TEMP_DIR/${SWIFT_PACKAGE}/usr" "$HOME/.swift/" + rm -rf "$TEMP_DIR" + + if ! grep -q 'swift' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Swift configuration' + echo 'export PATH="$HOME/.swift/usr/bin:$PATH"' + } >> "$HOME/.bashrc" + fi + export PATH="$HOME/.swift/usr/bin:$PATH" + else + rm -rf "$TEMP_DIR" + fi + fi +fi + +# --- Installation Summary --- +log_step "Installation Summary" + +echo "" +echo "System & Development Tools:" +command_exists gh && log_success "GitHub CLI: $(gh --version | head -n1)" || true +command_exists gh-setup-git-identity && log_success "gh-setup-git-identity: installed" || true +command_exists glab && log_success "GitLab CLI: $(glab --version | head -n1)" || true +command_exists glab-setup-git-identity && log_success "glab-setup-git-identity: installed" || true +command_exists git && log_success "Git: $(git --version)" || true +command_exists bun && log_success "Bun: $(bun --version)" || true +command_exists deno && log_success "Deno: $(deno --version | head -n1)" || true +command_exists node && log_success "Node.js: $(node --version)" || true +command_exists python && log_success "Python: $(python --version)" || true +command_exists go && log_success "Go: $(go version)" || true +command_exists rustc && log_success "Rust: $(rustc --version)" || true +command_exists java && log_success "Java: $(java -version 2>&1 | head -n1)" || true +command_exists kotlin && log_success "Kotlin: $(kotlin -version 2>&1 | head -n1)" || true +command_exists lean && log_success "Lean: $(lean --version)" || true +command_exists R && log_success "R: $(R --version | head -n1)" || true +command_exists ruby && log_success "Ruby: $(ruby --version)" || true +command_exists swift && log_success "Swift: $(swift --version 2>&1 | head -n1)" || true +command_exists brew && log_success "Homebrew: $(brew --version 2>/dev/null | head -n1)" || true +command_exists php && log_success "PHP: $(php --version 2>/dev/null | head -n1)" || true +command_exists perl && log_success "Perl: $(perl --version | head -n 2 | tail -n 1 | sed 's/^[[:space:]]*//')" || true +command_exists opam && log_success "Opam: $(opam --version)" || true + +echo "" +EOF_FULL_SETUP + +chmod +x /tmp/full-sandbox-user-setup.sh +if [ "$EUID" -eq 0 ]; then + su - sandbox -c "bash /tmp/full-sandbox-user-setup.sh" +else + sudo -i -u sandbox bash /tmp/full-sandbox-user-setup.sh +fi +rm -f /tmp/full-sandbox-user-setup.sh + +# --- Final cleanup --- +log_step "Final cleanup" +maybe_sudo apt-get clean +maybe_sudo apt-get autoclean +maybe_sudo apt-get autoremove -y +maybe_sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +log_step "Full Sandbox setup complete!" +log_success "All components installed successfully" +log_note "Please restart your shell or run: source ~/.bashrc" diff --git a/ubuntu/24.04/go/Dockerfile b/ubuntu/24.04/go/Dockerfile new file mode 100644 index 0000000..728aa6d --- /dev/null +++ b/ubuntu/24.04/go/Dockerfile @@ -0,0 +1,23 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/go/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-go . + +# Go sandbox: latest stable Go +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/go/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV GOROOT="/home/sandbox/.go" +ENV GOPATH="/home/sandbox/.go/path" +ENV PATH="/home/sandbox/.go/bin:/home/sandbox/.go/path/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/go/install.sh b/ubuntu/24.04/go/install.sh new file mode 100644 index 0000000..002949c --- /dev/null +++ b/ubuntu/24.04/go/install.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Go (Golang) installation +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Go" + +if [ ! -d "$HOME/.go" ] && [ ! -d "/usr/local/go" ]; then + log_info "Installing Golang..." + + ARCH=$(uname -m) + case "$ARCH" in + x86_64) GO_ARCH="amd64" ;; + aarch64) GO_ARCH="arm64" ;; + armv7l) GO_ARCH="armv6l" ;; + *) GO_ARCH="" ;; + esac + + if [ -n "$GO_ARCH" ]; then + GO_VERSION=$(curl -sL 'https://go.dev/VERSION?m=text' | head -n1) + + if [ -n "$GO_VERSION" ]; then + GO_TARBALL="${GO_VERSION}.linux-${GO_ARCH}.tar.gz" + GO_URL="https://go.dev/dl/${GO_TARBALL}" + + log_info "Downloading Go $GO_VERSION for $GO_ARCH..." + TEMP_DIR=$(mktemp -d) + curl -sL "$GO_URL" -o "$TEMP_DIR/$GO_TARBALL" + + log_info "Installing Go to $HOME/.go..." + mkdir -p "$HOME/.go" + tar -xzf "$TEMP_DIR/$GO_TARBALL" -C "$HOME/.go" --strip-components=1 + rm -rf "$TEMP_DIR" + + if ! grep -q 'GOROOT.*\.go' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Go configuration' + echo 'export GOROOT="$HOME/.go"' + echo 'export GOPATH="$HOME/.go/path"' + echo 'export PATH="$GOROOT/bin:$GOPATH/bin:$PATH"' + } >> "$HOME/.bashrc" + fi + + export GOROOT="$HOME/.go" + export GOPATH="$HOME/.go/path" + export PATH="$GOROOT/bin:$GOPATH/bin:$PATH" + mkdir -p "$GOPATH" + + if command -v go &>/dev/null; then + log_success "Golang installed: $(go version)" + fi + fi + fi +else + log_info "Golang already installed." +fi + +log_success "Go installation complete" diff --git a/ubuntu/24.04/java/Dockerfile b/ubuntu/24.04/java/Dockerfile new file mode 100644 index 0000000..f22d4d3 --- /dev/null +++ b/ubuntu/24.04/java/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/java/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-java . + +# Java sandbox: SDKMAN + Eclipse Temurin 21 +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/java/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV SDKMAN_DIR="/home/sandbox/.sdkman" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/java/install.sh b/ubuntu/24.04/java/install.sh new file mode 100644 index 0000000..89616ae --- /dev/null +++ b/ubuntu/24.04/java/install.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Java installation via SDKMAN (Eclipse Temurin 21 LTS) +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Java via SDKMAN" + +# --- SDKMAN --- +if [ ! -d "$HOME/.sdkman" ]; then + log_info "Installing SDKMAN (Java version manager)..." + curl -s "https://get.sdkman.io?rcupdate=false&ci=true" | bash + if ! grep -q 'sdkman-init.sh' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# SDKMAN configuration' + echo 'export SDKMAN_DIR="$HOME/.sdkman"' + echo '[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"' + } >> "$HOME/.bashrc" + fi + log_success "SDKMAN installed and configured" +else + log_info "SDKMAN already installed." +fi + +# Load SDKMAN and install Java +export SDKMAN_DIR="$HOME/.sdkman" +if [ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]; then + set +u + source "$SDKMAN_DIR/bin/sdkman-init.sh" + set -u + log_success "SDKMAN loaded for current session" + + log_info "Installing Java 21 LTS (OpenJDK via Eclipse Temurin)..." + set +u + if ! sdk list java 2>/dev/null | grep -q "21.*tem.*installed"; then + sdk install java 21-tem < /dev/null || { + log_warning "Eclipse Temurin installation failed, trying default OpenJDK..." + sdk install java 21-open < /dev/null || true + } + else + log_info "Java 21 (Temurin) already installed." + fi + set -u + + if command -v java &>/dev/null; then + log_success "Java version manager setup complete" + java -version 2>&1 | head -n1 + fi +fi + +log_success "Java installation complete" diff --git a/ubuntu/24.04/js/Dockerfile b/ubuntu/24.04/js/Dockerfile new file mode 100644 index 0000000..c89e974 --- /dev/null +++ b/ubuntu/24.04/js/Dockerfile @@ -0,0 +1,40 @@ +FROM ubuntu:24.04 + +# JavaScript/TypeScript sandbox: Node.js (NVM), Bun, Deno +# Standalone JS image without essentials overhead +# Published as: konard/sandbox-js +# +# Build from repository root: +# docker build -f ubuntu/24.04/js/Dockerfile -t sandbox-js . + +ENV DEBIAN_FRONTEND=noninteractive +WORKDIR /workspace + +# Install minimal system prerequisites +RUN apt update -y && \ + apt install -y curl git sudo ca-certificates unzip && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create sandbox user +RUN useradd -m -s /bin/bash sandbox && \ + passwd -d sandbox && \ + usermod -aG sudo sandbox + +# Copy and run install script +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/js/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + su - sandbox -c "bash /tmp/install.sh" && \ + rm -f /tmp/install.sh /tmp/common.sh + +USER sandbox +WORKDIR /home/sandbox + +ENV NVM_DIR="/home/sandbox/.nvm" +ENV BUN_INSTALL="/home/sandbox/.bun" +ENV DENO_INSTALL="/home/sandbox/.deno" +ENV PATH="/home/sandbox/.deno/bin:/home/sandbox/.bun/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] + +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/js/install.sh b/ubuntu/24.04/js/install.sh new file mode 100644 index 0000000..e556509 --- /dev/null +++ b/ubuntu/24.04/js/install.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# JavaScript/TypeScript runtime installation (Node.js via NVM, Bun, Deno) +# Usage: curl -fsSL | bash OR bash install.sh +# Requires: curl, git (should be pre-installed on Ubuntu 24.04) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing JavaScript/TypeScript runtimes" + +# --- Bun --- +if ! command_exists bun; then + log_info "Installing Bun..." + curl -fsSL https://bun.sh/install | bash + export BUN_INSTALL="$HOME/.bun" + export PATH="$BUN_INSTALL/bin:$PATH" + log_success "Bun installed" +else + log_info "Bun already installed." +fi + +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" + +# --- Deno --- +if ! command_exists deno; then + log_info "Installing Deno..." + curl -fsSL https://deno.land/install.sh | sh -s -- -y + export DENO_INSTALL="$HOME/.deno" + export PATH="$DENO_INSTALL/bin:$PATH" + if ! grep -q 'DENO_INSTALL' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Deno configuration' + echo 'export DENO_INSTALL="$HOME/.deno"' + echo 'export PATH="$DENO_INSTALL/bin:$PATH"' + } >> "$HOME/.bashrc" + fi + log_success "Deno installed" +else + log_info "Deno already installed." +fi + +# --- NVM + Node.js --- +if [ ! -d "$HOME/.nvm" ]; then + log_info "Installing NVM..." + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash + log_success "NVM installed" +else + log_info "NVM already installed." +fi + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + +if ! nvm ls 20 2>/dev/null | grep -q 'v20'; then + log_info "Installing Node.js 20..." + nvm install 20 + log_success "Node.js 20 installed" +else + log_info "Node.js 20 already installed" +fi +nvm use 20 + +log_info "Updating npm to latest version..." +npm install -g npm@latest --no-fund --silent +log_success "npm updated to latest version" + +log_success "JavaScript/TypeScript runtimes installation complete" diff --git a/ubuntu/24.04/kotlin/Dockerfile b/ubuntu/24.04/kotlin/Dockerfile new file mode 100644 index 0000000..704c97d --- /dev/null +++ b/ubuntu/24.04/kotlin/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/kotlin/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-kotlin . + +# Kotlin sandbox: SDKMAN + Kotlin +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/kotlin/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV SDKMAN_DIR="/home/sandbox/.sdkman" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/kotlin/install.sh b/ubuntu/24.04/kotlin/install.sh new file mode 100644 index 0000000..6c0acb6 --- /dev/null +++ b/ubuntu/24.04/kotlin/install.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Kotlin installation via SDKMAN +# Usage: curl -fsSL | bash OR bash install.sh +# Requires: SDKMAN (install java first, or SDKMAN will be installed here) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Kotlin via SDKMAN" + +# Ensure SDKMAN is installed +if [ ! -d "$HOME/.sdkman" ]; then + log_info "SDKMAN not found, installing..." + curl -s "https://get.sdkman.io?rcupdate=false&ci=true" | bash + if ! grep -q 'sdkman-init.sh' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# SDKMAN configuration' + echo 'export SDKMAN_DIR="$HOME/.sdkman"' + echo '[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"' + } >> "$HOME/.bashrc" + fi +fi + +export SDKMAN_DIR="$HOME/.sdkman" +if [ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]; then + set +u + source "$SDKMAN_DIR/bin/sdkman-init.sh" + set -u + + if ! command_exists kotlin; then + log_info "Installing Kotlin via SDKMAN..." + set +u + sdk install kotlin < /dev/null || true + set -u + + if command -v kotlin &>/dev/null; then + log_success "Kotlin installed: $(kotlin -version 2>&1 | head -n1)" + fi + else + log_info "Kotlin already installed." + fi +fi + +log_success "Kotlin installation complete" diff --git a/ubuntu/24.04/lean/Dockerfile b/ubuntu/24.04/lean/Dockerfile new file mode 100644 index 0000000..9d54e04 --- /dev/null +++ b/ubuntu/24.04/lean/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/lean/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-lean . + +# Lean sandbox: Lean theorem prover via elan +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/lean/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV PATH="/home/sandbox/.elan/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/lean/install.sh b/ubuntu/24.04/lean/install.sh new file mode 100644 index 0000000..07769ec --- /dev/null +++ b/ubuntu/24.04/lean/install.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Lean theorem prover installation via elan +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Lean via elan" + +if [ ! -d "$HOME/.elan" ]; then + log_info "Installing Lean (via elan)..." + curl https://elan.lean-lang.org/elan-init.sh -sSf | sh -s -- -y --default-toolchain stable + if [ -f "$HOME/.elan/env" ]; then + \. "$HOME/.elan/env" + log_success "Lean installed successfully" + fi + if ! grep -q 'elan' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Lean (elan) configuration' + echo 'export PATH="$HOME/.elan/bin:$PATH"' + } >> "$HOME/.bashrc" + fi +else + log_info "Lean (elan) already installed." +fi + +log_success "Lean installation complete" diff --git a/ubuntu/24.04/perl/Dockerfile b/ubuntu/24.04/perl/Dockerfile new file mode 100644 index 0000000..a2b8a4f --- /dev/null +++ b/ubuntu/24.04/perl/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/perl/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-perl . + +# Perl sandbox: Perlbrew + latest stable Perl +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/perl/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV PERLBREW_ROOT="/home/sandbox/.perl5" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/perl/install.sh b/ubuntu/24.04/perl/install.sh new file mode 100644 index 0000000..903fb4c --- /dev/null +++ b/ubuntu/24.04/perl/install.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Perl installation via Perlbrew +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Perl via Perlbrew" + +if [ ! -d "$HOME/.perl5" ]; then + log_info "Installing Perlbrew (Perl version manager)..." + + export PERLBREW_ROOT="$HOME/.perl5" + curl -L https://install.perlbrew.pl | bash + + if ! grep -q 'perlbrew' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Perlbrew configuration' + echo 'if [ -n "$PS1" ]; then' + echo ' export PERLBREW_ROOT="$HOME/.perl5"' + echo ' [ -f "$PERLBREW_ROOT/etc/bashrc" ] && source "$PERLBREW_ROOT/etc/bashrc"' + echo 'fi' + } >> "$HOME/.bashrc" + fi + + if [ -f "$PERLBREW_ROOT/etc/bashrc" ]; then + sed -i 's/\$1/${1:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + sed -i 's/\$PERLBREW_LIB/${PERLBREW_LIB:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + sed -i 's/\$outsep/${outsep:-}/g' "$PERLBREW_ROOT/etc/bashrc" 2>/dev/null || true + + set +u + source "$PERLBREW_ROOT/etc/bashrc" + set -u + log_success "Perlbrew installed and configured" + + log_info "Installing latest stable Perl version..." + PERLBREW_OUTPUT=$(perlbrew available 2>&1 || true) + LATEST_PERL=$(echo "$PERLBREW_OUTPUT" | grep -oE 'perl-5\.[0-9]+\.[0-9]+' | head -1 || true) + + if [ -n "$LATEST_PERL" ]; then + log_info "Installing $LATEST_PERL..." + if ! perlbrew list | grep -q "$LATEST_PERL"; then + perlbrew install "$LATEST_PERL" --notest || true + fi + + if perlbrew list | grep -q "$LATEST_PERL"; then + perlbrew switch "$LATEST_PERL" + log_success "Perl version manager setup complete" + fi + fi + fi +else + log_info "Perlbrew already installed." +fi + +log_success "Perl installation complete" diff --git a/ubuntu/24.04/php/Dockerfile b/ubuntu/24.04/php/Dockerfile new file mode 100644 index 0000000..704e30b --- /dev/null +++ b/ubuntu/24.04/php/Dockerfile @@ -0,0 +1,25 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/php/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-php . + +# PHP sandbox: PHP 8.3 via Homebrew +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root +RUN mkdir -p /home/linuxbrew/.linuxbrew && \ + chown -R sandbox:sandbox /home/linuxbrew + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/php/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV PATH="/home/linuxbrew/.linuxbrew/opt/php@8.3/bin:/home/linuxbrew/.linuxbrew/opt/php@8.3/sbin:/home/linuxbrew/.linuxbrew/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/php/install.sh b/ubuntu/24.04/php/install.sh new file mode 100644 index 0000000..94e986e --- /dev/null +++ b/ubuntu/24.04/php/install.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# PHP 8.3 installation via Homebrew +# Usage: curl -fsSL | bash OR bash install.sh +# Requires: Homebrew (will be installed if not present) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing PHP 8.3 via Homebrew" + +# Ensure Homebrew directory exists +if [ ! -d /home/linuxbrew/.linuxbrew ]; then + log_info "Creating Homebrew directory..." + maybe_sudo mkdir -p /home/linuxbrew/.linuxbrew + maybe_sudo chown -R "$(whoami)":"$(whoami)" /home/linuxbrew 2>/dev/null || true +fi + +# Install Homebrew if not present +if ! command_exists brew; then + log_info "Installing Homebrew..." + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 2>&1 || true + + if [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + elif [[ -x "$HOME/.linuxbrew/bin/brew" ]]; then + eval "$("$HOME/.linuxbrew/bin/brew" shellenv)" + fi + + if ! grep -q "brew shellenv" "$HOME/.bashrc" 2>/dev/null; then + BREW_PREFIX=$(brew --prefix 2>/dev/null || echo "/home/linuxbrew/.linuxbrew") + echo "eval \"\$($BREW_PREFIX/bin/brew shellenv)\"" >> "$HOME/.bashrc" + fi +else + eval "$(brew shellenv 2>/dev/null)" || true +fi + +# Install PHP via Homebrew +if command_exists brew; then + if ! brew list --formula 2>/dev/null | grep -q "^php@"; then + log_info "Installing PHP via Homebrew..." + + if ! brew tap | grep -q "shivammathur/php"; then + brew tap shivammathur/php || true + fi + + if brew tap | grep -q "shivammathur/php"; then + export HOMEBREW_NO_ANALYTICS=1 + export HOMEBREW_NO_AUTO_UPDATE=1 + + log_info "Installing PHP 8.3..." + brew install shivammathur/php/php@8.3 || true + + if brew list --formula 2>/dev/null | grep -q "^php@8.3$"; then + brew link --overwrite --force shivammathur/php/php@8.3 2>&1 | grep -v "Warning" || true + + BREW_PREFIX=$(brew --prefix 2>/dev/null || echo "") + if [[ -n "$BREW_PREFIX" && -d "$BREW_PREFIX/opt/php@8.3" ]]; then + export PATH="$BREW_PREFIX/opt/php@8.3/bin:$BREW_PREFIX/opt/php@8.3/sbin:$PATH" + + if ! grep -q "php@8.3/bin" "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" << 'PHP_PATH_EOF' + +# PHP 8.3 PATH configuration +export PATH="$(brew --prefix)/opt/php@8.3/bin:$(brew --prefix)/opt/php@8.3/sbin:$PATH" +PHP_PATH_EOF + fi + fi + + if command -v php &>/dev/null; then + PHP_VERSION=$(php --version 2>/dev/null | head -n 1 || echo "unknown version") + log_success "PHP installed and available: $PHP_VERSION" + fi + fi + fi + else + log_info "PHP already installed via Homebrew." + fi +fi + +log_success "PHP installation complete" diff --git a/ubuntu/24.04/python/Dockerfile b/ubuntu/24.04/python/Dockerfile new file mode 100644 index 0000000..885280d --- /dev/null +++ b/ubuntu/24.04/python/Dockerfile @@ -0,0 +1,22 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/python/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-python . + +# Python sandbox: Pyenv + latest stable Python +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/python/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV PYENV_ROOT="/home/sandbox/.pyenv" +ENV PATH="/home/sandbox/.pyenv/bin:/home/sandbox/.pyenv/shims:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/python/install.sh b/ubuntu/24.04/python/install.sh new file mode 100644 index 0000000..6797fd6 --- /dev/null +++ b/ubuntu/24.04/python/install.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Python installation via Pyenv +# Usage: curl -fsSL | bash OR bash install.sh +# Requires: essentials-sandbox (provides build dependencies: libssl-dev, zlib1g-dev, etc.) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Python via Pyenv" + +# Note: Build dependencies (libssl-dev, zlib1g-dev, libbz2-dev, libreadline-dev, +# libsqlite3-dev, libncursesw5-dev, xz-utils, tk-dev, libxml2-dev, libxmlsec1-dev, +# libffi-dev, liblzma-dev) are provided by essentials-sandbox. + +# --- Pyenv --- +if [ ! -d "$HOME/.pyenv" ]; then + log_info "Installing Pyenv..." + curl https://pyenv.run | bash + if ! grep -q 'pyenv init' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Pyenv configuration' + echo 'export PYENV_ROOT="$HOME/.pyenv"' + echo 'export PATH="$PYENV_ROOT/bin:$PATH"' + echo 'eval "$(pyenv init --path)"' + echo 'eval "$(pyenv init -)"' + } >> "$HOME/.bashrc" + fi + log_success "Pyenv installed and configured" +else + log_info "Pyenv already installed." +fi + +# Load pyenv for current session +export PYENV_ROOT="$HOME/.pyenv" +export PATH="$PYENV_ROOT/bin:$PATH" +if command -v pyenv >/dev/null 2>&1; then + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + log_success "Pyenv loaded for current session" + + # Install latest stable Python version + log_info "Installing latest stable Python version..." + LATEST_PYTHON=$(pyenv install --list | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]') + + if [ -n "$LATEST_PYTHON" ]; then + log_info "Installing Python $LATEST_PYTHON..." + if ! pyenv versions --bare | grep -q "^${LATEST_PYTHON}$"; then + pyenv install "$LATEST_PYTHON" + else + log_info "Python $LATEST_PYTHON already installed." + fi + + log_info "Setting Python $LATEST_PYTHON as global default..." + pyenv global "$LATEST_PYTHON" + log_success "Python version manager setup complete" + python --version + fi +fi + +log_success "Python installation complete" diff --git a/ubuntu/24.04/r/Dockerfile b/ubuntu/24.04/r/Dockerfile new file mode 100644 index 0000000..dc8827f --- /dev/null +++ b/ubuntu/24.04/r/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/r/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-r . + +# R sandbox: R statistical language +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root + +COPY ubuntu/24.04/common.sh /tmp/common.sh +COPY ubuntu/24.04/r/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +USER sandbox +WORKDIR /home/sandbox + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/r/install.sh b/ubuntu/24.04/r/install.sh new file mode 100644 index 0000000..c9645b4 --- /dev/null +++ b/ubuntu/24.04/r/install.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# R language installation +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing R" + +if ! command_exists R; then + log_info "Installing R statistical language..." + maybe_sudo apt install -y r-base + log_success "R language installed" +else + log_info "R already installed." +fi + +log_success "R installation complete" diff --git a/ubuntu/24.04/rocq/Dockerfile b/ubuntu/24.04/rocq/Dockerfile new file mode 100644 index 0000000..f00eec2 --- /dev/null +++ b/ubuntu/24.04/rocq/Dockerfile @@ -0,0 +1,28 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/rocq/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-rocq . + +# Rocq/Coq sandbox: Opam + Rocq theorem prover +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER root +RUN apt-get update -y && apt-get install -y bubblewrap && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/rocq/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV OPAM_SWITCH_PREFIX="/home/sandbox/.opam/default" +ENV CAML_LD_LIBRARY_PATH="/home/sandbox/.opam/default/lib/stublibs:/home/sandbox/.opam/default/lib/ocaml/stublibs:/home/sandbox/.opam/default/lib/ocaml" +ENV OCAML_TOPLEVEL_PATH="/home/sandbox/.opam/default/lib/toplevel" +ENV PATH="/home/sandbox/.opam/default/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/rocq/install.sh b/ubuntu/24.04/rocq/install.sh new file mode 100644 index 0000000..8f717e8 --- /dev/null +++ b/ubuntu/24.04/rocq/install.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Rocq/Coq theorem prover installation via Opam +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Rocq/Coq via Opam" + +# Note: bubblewrap is provided by essentials-sandbox or the Dockerfile. + +# --- Opam --- +if ! command_exists opam; then + log_info "Installing Opam (OCaml package manager)..." + + # Install opam binary to user-writable location + OPAM_BIN_DIR="$HOME/.local/bin" + mkdir -p "$OPAM_BIN_DIR" + export PATH="$OPAM_BIN_DIR:$PATH" + # The install script asks: 1) path [/usr/local/bin], 2) create dir? [Y/n] + printf '%s\n%s\n' "$OPAM_BIN_DIR" "Y" | bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh) --no-backup" || { + # Fallback: download opam binary directly + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) OPAM_ARCH="x86_64" ;; + aarch64) OPAM_ARCH="arm64" ;; + *) OPAM_ARCH="$ARCH" ;; + esac + curl -fsSL "https://github.com/ocaml/opam/releases/latest/download/opam-2.3.0-${OPAM_ARCH}-linux" -o "$OPAM_BIN_DIR/opam" && \ + chmod +x "$OPAM_BIN_DIR/opam" || true + } + + if command_exists opam; then + log_success "Opam installed successfully" + fi +else + log_info "Opam already installed." +fi + +# Initialize opam and install Rocq +if command_exists opam; then + if [ ! -d "$HOME/.opam" ]; then + log_info "Initializing Opam..." + opam init --disable-sandboxing --auto-setup -y || true + log_success "Opam initialized" + fi + + eval "$(opam env --switch=default 2>/dev/null)" || true + + ROCQ_ACCESSIBLE=false + if command -v rocq &>/dev/null && rocq -v &>/dev/null; then + ROCQ_ACCESSIBLE=true + elif command -v rocqc &>/dev/null; then + ROCQ_ACCESSIBLE=true + elif command -v coqc &>/dev/null; then + ROCQ_ACCESSIBLE=true + fi + + if [ "$ROCQ_ACCESSIBLE" = false ]; then + log_info "Installing Rocq Prover (this may take several minutes)..." + opam repo add rocq-released https://rocq-prover.org/opam/released 2>/dev/null || true + opam update 2>/dev/null || true + + if opam pin add rocq-prover --yes 2>/dev/null; then + log_success "Rocq Prover pinned and installed" + elif opam install rocq-prover -y 2>/dev/null; then + log_success "Rocq Prover installed via opam install" + else + opam install coq -y || true + fi + + eval "$(opam env --switch=default 2>/dev/null)" || true + else + log_info "Rocq Prover already installed." + fi + + if ! grep -q 'opam env' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Opam (OCaml/Rocq) configuration' + echo 'test -r $HOME/.opam/opam-init/init.sh && . $HOME/.opam/opam-init/init.sh > /dev/null 2> /dev/null || true' + } >> "$HOME/.bashrc" + fi +fi + +# Ensure .opam directory exists (required for COPY --from in full-sandbox) +if [ ! -d "$HOME/.opam" ]; then + log_warning "Opam directory not found - creating minimal structure" + mkdir -p "$HOME/.opam" +fi + +log_success "Rocq/Coq installation complete" diff --git a/ubuntu/24.04/ruby/Dockerfile b/ubuntu/24.04/ruby/Dockerfile new file mode 100644 index 0000000..0a1b0ea --- /dev/null +++ b/ubuntu/24.04/ruby/Dockerfile @@ -0,0 +1,22 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/ruby/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-ruby . + +# Ruby sandbox: rbenv + latest stable Ruby 3.x +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries including libyaml-dev) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/ruby/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV RBENV_ROOT="/home/sandbox/.rbenv" +ENV PATH="/home/sandbox/.rbenv/bin:/home/sandbox/.rbenv/shims:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/ruby/install.sh b/ubuntu/24.04/ruby/install.sh new file mode 100644 index 0000000..8111b73 --- /dev/null +++ b/ubuntu/24.04/ruby/install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Ruby installation via rbenv +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } + maybe_sudo() { if [ "$EUID" -eq 0 ]; then "$@"; elif command -v sudo &>/dev/null; then sudo "$@"; else "$@"; fi; } +fi + +log_step "Installing Ruby via rbenv" + +# Note: Build dependencies (libyaml-dev, libssl-dev, etc.) are provided by essentials-sandbox. + +if [ ! -d "$HOME/.rbenv" ]; then + log_info "Installing rbenv (Ruby version manager)..." + + git clone https://github.com/rbenv/rbenv.git "$HOME/.rbenv" + mkdir -p "$HOME/.rbenv/plugins" + git clone https://github.com/rbenv/ruby-build.git "$HOME/.rbenv/plugins/ruby-build" + + if ! grep -q 'rbenv init' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# rbenv configuration' + echo 'export PATH="$HOME/.rbenv/bin:$PATH"' + echo 'eval "$(rbenv init - bash)"' + } >> "$HOME/.bashrc" + fi + + export PATH="$HOME/.rbenv/bin:$PATH" + eval "$(rbenv init - bash)" + log_success "rbenv installed and configured" + + # Install latest stable Ruby 3.x version (avoid pre-release 4.x) + log_info "Installing latest stable Ruby version..." + LATEST_RUBY=$(rbenv install -l 2>/dev/null | grep -E '^\s*3\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]') + + if [ -n "$LATEST_RUBY" ]; then + log_info "Installing Ruby $LATEST_RUBY..." + if ! rbenv versions | grep -q "$LATEST_RUBY"; then + rbenv install "$LATEST_RUBY" + else + log_info "Ruby $LATEST_RUBY already installed." + fi + + rbenv global "$LATEST_RUBY" + log_success "Ruby version manager setup complete" + ruby --version + fi +else + log_info "rbenv already installed." +fi + +log_success "Ruby installation complete" diff --git a/ubuntu/24.04/rust/Dockerfile b/ubuntu/24.04/rust/Dockerfile new file mode 100644 index 0000000..8bfe2b0 --- /dev/null +++ b/ubuntu/24.04/rust/Dockerfile @@ -0,0 +1,22 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/rust/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-rust . + +# Rust sandbox: rustup + latest stable Rust +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/rust/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV CARGO_HOME="/home/sandbox/.cargo" +ENV PATH="/home/sandbox/.cargo/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/rust/install.sh b/ubuntu/24.04/rust/install.sh new file mode 100644 index 0000000..7da84a5 --- /dev/null +++ b/ubuntu/24.04/rust/install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Rust installation via rustup +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Rust" + +if [ ! -d "$HOME/.cargo" ]; then + log_info "Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + if [ -f "$HOME/.cargo/env" ]; then + \. "$HOME/.cargo/env" + log_success "Rust installed successfully" + fi +else + log_info "Rust already installed." +fi + +log_success "Rust installation complete" diff --git a/ubuntu/24.04/swift/Dockerfile b/ubuntu/24.04/swift/Dockerfile new file mode 100644 index 0000000..fcfafd3 --- /dev/null +++ b/ubuntu/24.04/swift/Dockerfile @@ -0,0 +1,21 @@ +ARG ESSENTIALS_IMAGE=konard/sandbox-essentials:latest +FROM ${ESSENTIALS_IMAGE} +# Build: docker build -f ubuntu/24.04/swift/Dockerfile --build-arg ESSENTIALS_IMAGE=sandbox-essentials -t sandbox-swift . + +# Swift sandbox: Swift 6.0.3 +# Built on top of essentials-sandbox (inherits JS, git, gh, glab, dev libraries) + +USER sandbox + +COPY --chown=sandbox:sandbox ubuntu/24.04/common.sh /tmp/common.sh +COPY --chown=sandbox:sandbox ubuntu/24.04/swift/install.sh /tmp/install.sh +RUN chmod +x /tmp/install.sh /tmp/common.sh && \ + bash /tmp/install.sh && \ + rm -f /tmp/install.sh /tmp/common.sh + +WORKDIR /home/sandbox + +ENV PATH="/home/sandbox/.swift/usr/bin:${PATH}" + +SHELL ["/bin/bash", "-c"] +CMD ["/bin/bash"] diff --git a/ubuntu/24.04/swift/install.sh b/ubuntu/24.04/swift/install.sh new file mode 100644 index 0000000..b64aa17 --- /dev/null +++ b/ubuntu/24.04/swift/install.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Swift installation +# Usage: curl -fsSL | bash OR bash install.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/../common.sh" ]; then + source "$SCRIPT_DIR/../common.sh" +elif [ -f "/tmp/common.sh" ]; then + source "/tmp/common.sh" +else + set -euo pipefail + log_info() { echo "[*] $1"; } + log_success() { echo "[✓] $1"; } + log_warning() { echo "[!] $1"; } + log_error() { echo "[✗] $1"; } + log_step() { echo "==> $1"; } + command_exists() { command -v "$1" &>/dev/null; } +fi + +log_step "Installing Swift" + +if ! command_exists swift; then + log_info "Installing Swift..." + + ARCH=$(uname -m) + case "$ARCH" in + x86_64) + SWIFT_DIR="ubuntu2404" + SWIFT_FILE_SUFFIX="ubuntu24.04" + ;; + aarch64) + SWIFT_DIR="ubuntu2404-aarch64" + SWIFT_FILE_SUFFIX="ubuntu24.04-aarch64" + ;; + *) + SWIFT_DIR="" + SWIFT_FILE_SUFFIX="" + ;; + esac + + if [ -n "$SWIFT_DIR" ]; then + SWIFT_VERSION="6.0.3" + SWIFT_RELEASE="RELEASE" + SWIFT_PACKAGE="swift-${SWIFT_VERSION}-${SWIFT_RELEASE}-${SWIFT_FILE_SUFFIX}" + SWIFT_URL="https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_DIR}/swift-${SWIFT_VERSION}-${SWIFT_RELEASE}/${SWIFT_PACKAGE}.tar.gz" + + log_info "Downloading Swift $SWIFT_VERSION for $ARCH..." + TEMP_DIR=$(mktemp -d) + + if curl -fsSL "$SWIFT_URL" -o "$TEMP_DIR/swift.tar.gz"; then + log_info "Installing Swift to $HOME/.swift..." + mkdir -p "$HOME/.swift" + tar -xzf "$TEMP_DIR/swift.tar.gz" -C "$TEMP_DIR" + cp -r "$TEMP_DIR/${SWIFT_PACKAGE}/usr" "$HOME/.swift/" + rm -rf "$TEMP_DIR" + + if ! grep -q 'swift' "$HOME/.bashrc" 2>/dev/null; then + { + echo '' + echo '# Swift configuration' + echo 'export PATH="$HOME/.swift/usr/bin:$PATH"' + } >> "$HOME/.bashrc" + fi + + export PATH="$HOME/.swift/usr/bin:$PATH" + + if command -v swift &>/dev/null; then + log_success "Swift installed: $(swift --version | head -n1)" + fi + else + log_error "Failed to download Swift from $SWIFT_URL" + rm -rf "$TEMP_DIR" + fi + else + log_warning "Swift installation skipped: unsupported architecture $ARCH" + fi +else + log_info "Swift already installed." +fi + +log_success "Swift installation complete"