Skip to content

Commit 5358fa3

Browse files
Copilotabdurriq
andcommitted
Add sync mechanism for shared code with single source of truth
Created scripts/lib/common-setup.sh as the source of truth and scripts/sync-common-setup.sh to deploy to all features. This provides a shared code model within devcontainer packaging constraints. Co-authored-by: abdurriq <[email protected]>
1 parent 67e27ee commit 5358fa3

4 files changed

Lines changed: 315 additions & 0 deletions

File tree

SHARED_CODE.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Shared Code Maintenance
2+
3+
This document explains how shared code is maintained across features in this repository.
4+
5+
## Problem
6+
7+
Multiple features need the same helper functions (e.g., user selection logic). The devcontainer specification currently packages each feature independently and doesn't support sharing code between features at runtime.
8+
9+
## Solution
10+
11+
We maintain a **single source of truth** with a **sync mechanism** to deploy to each feature:
12+
13+
### Single Source
14+
- **Location**: `scripts/lib/common-setup.sh`
15+
- **Contains**: Shared helper functions (currently user selection logic)
16+
- **Maintenance**: All updates happen here
17+
18+
### Deployment
19+
- **Mechanism**: `scripts/sync-common-setup.sh`
20+
- **Target**: Copies to each feature's `_lib/` directory
21+
- **Reason**: Devcontainer packaging requires files to be within each feature's directory
22+
23+
## Workflow
24+
25+
### Making Changes
26+
27+
1. **Edit the source**: Modify `scripts/lib/common-setup.sh`
28+
2. **Test**: Run `bash test/_lib/test-common-setup.sh`
29+
3. **Sync**: Run `./scripts/sync-common-setup.sh`
30+
4. **Commit**: Include both source and deployed copies
31+
32+
```bash
33+
# Edit the source
34+
vim scripts/lib/common-setup.sh
35+
36+
# Test
37+
bash test/_lib/test-common-setup.sh
38+
39+
# Deploy to all features
40+
./scripts/sync-common-setup.sh
41+
42+
# Commit everything
43+
git add scripts/lib/common-setup.sh src/*/_lib/common-setup.sh
44+
git commit -m "Update common-setup.sh helper function"
45+
```
46+
47+
### Verification
48+
49+
The sync script is idempotent - running it multiple times with the same source produces the same result. After syncing, you can verify:
50+
51+
```bash
52+
# Check that all copies are identical
53+
for f in src/*/_lib/common-setup.sh; do
54+
diff -q scripts/lib/common-setup.sh "$f" || echo "MISMATCH: $f"
55+
done
56+
```
57+
58+
## Why Not Use Shared Files?
59+
60+
The devcontainer CLI packages each feature independently. When a feature is installed:
61+
62+
1. Only files within the feature's directory are included in the package
63+
2. Parent directories (`../common`) are not accessible
64+
3. Hidden directories (`.common`) are excluded from packaging
65+
4. Sibling feature directories are not accessible
66+
67+
This is a design decision in the devcontainer specification to ensure features are portable and self-contained.
68+
69+
## Future
70+
71+
The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` ([spec#129](https://github.com/devcontainers/spec/issues/129)) that would enable native code sharing. Once implemented, the sync mechanism can be removed in favor of declarative includes:
72+
73+
```json
74+
{
75+
"id": "my-feature",
76+
"include": ["../../scripts/lib/common-setup.sh"]
77+
}
78+
```
79+
80+
## Current Implementation
81+
82+
As of this PR:
83+
- **Source**: `scripts/lib/common-setup.sh` (87 lines)
84+
- **Deployed**: 17 features, each with `src/FEATURE/_lib/common-setup.sh`
85+
- **Sync Script**: `scripts/sync-common-setup.sh`
86+
- **Tests**: `test/_lib/test-common-setup.sh` (14 test cases)
87+
- **Benefits**: Eliminated ~188 lines of inline duplicated logic from install scripts
88+
89+
## References
90+
91+
- [Devcontainer Spec Issue #129 - Share code between features](https://github.com/devcontainers/spec/issues/129)
92+
- [Features Library Proposal](https://github.com/devcontainers/spec/blob/main/proposals/features-library.md)
93+
- Test documentation: `test/_lib/README.md`
94+
- Sync script documentation: `scripts/README.md`

scripts/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Shared Feature Code
2+
3+
This directory contains code that is shared across multiple features.
4+
5+
## Structure
6+
7+
```
8+
scripts/
9+
├── lib/
10+
│ └── common-setup.sh # Source of truth for user selection helper
11+
└── sync-common-setup.sh # Script to deploy helper to all features
12+
```
13+
14+
## Maintenance
15+
16+
### The Source of Truth
17+
18+
**`scripts/lib/common-setup.sh`** is the single source of truth for the user selection helper function. All modifications should be made to this file.
19+
20+
### Deploying Changes
21+
22+
Due to the devcontainer CLI's packaging behavior (each feature is packaged independently), the helper must be deployed to each feature's `_lib/` directory. We maintain this through a sync script:
23+
24+
```bash
25+
./scripts/sync-common-setup.sh
26+
```
27+
28+
This copies `scripts/lib/common-setup.sh` to all features:
29+
- `src/anaconda/_lib/common-setup.sh`
30+
- `src/docker-in-docker/_lib/common-setup.sh`
31+
- etc.
32+
33+
### Workflow
34+
35+
1. **Edit**: Make changes to `scripts/lib/common-setup.sh`
36+
2. **Test**: Run `bash test/_lib/test-common-setup.sh` to verify
37+
3. **Sync**: Run `./scripts/sync-common-setup.sh` to deploy to all features
38+
4. **Commit**: Commit both the source and all copies together
39+
40+
### Why Copies?
41+
42+
The devcontainer CLI packages each feature independently:
43+
- Parent directories are not included in the build context
44+
- Hidden directories (`.common`) are not included
45+
- Sibling directories are not accessible
46+
47+
Therefore, each feature needs its own copy of the helper to ensure it's available at runtime during feature installation.
48+
49+
## Testing
50+
51+
Tests are located in `test/_lib/` and reference the anaconda feature's copy as the source:
52+
53+
```bash
54+
bash test/_lib/test-common-setup.sh
55+
```
56+
57+
## Future
58+
59+
This approach is a workaround for the current limitation. The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` that would allow native code sharing (see [devcontainers/spec#129](https://github.com/devcontainers/spec/issues/129)). Once implemented, this sync mechanism can be removed.

scripts/lib/common-setup.sh

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
#-------------------------------------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See https://github.com/devcontainers/features/blob/main/LICENSE for license information.
5+
#-------------------------------------------------------------------------------------------------------------------------
6+
#
7+
# Helper script for common feature setup tasks, including user selection logic.
8+
# Maintainer: The Dev Container spec maintainers
9+
10+
# Determine the appropriate non-root user
11+
# Usage: determine_user_from_input USERNAME [FALLBACK_USER]
12+
#
13+
# This function resolves the USERNAME variable based on the input value:
14+
# - If USERNAME is "auto" or "automatic", it will detect an existing non-root user
15+
# - If USERNAME is "none" or doesn't exist, it will fall back to root
16+
# - Otherwise, it validates the specified USERNAME exists
17+
#
18+
# Arguments:
19+
# USERNAME - The username input (typically from feature configuration)
20+
# FALLBACK_USER - Optional fallback user when no user is found in automatic mode (defaults to "root")
21+
#
22+
# Returns:
23+
# The resolved username is printed to stdout
24+
#
25+
# Examples:
26+
# USERNAME=$(determine_user_from_input "automatic")
27+
# USERNAME=$(determine_user_from_input "vscode")
28+
# USERNAME=$(determine_user_from_input "auto" "vscode")
29+
#
30+
determine_user_from_input() {
31+
local input_username="${1:-automatic}"
32+
local fallback_user="${2:-root}"
33+
local resolved_username=""
34+
35+
if [ "${input_username}" = "auto" ] || [ "${input_username}" = "automatic" ]; then
36+
# Automatic mode: try to detect an existing non-root user
37+
38+
# First, check if _REMOTE_USER is set and is not root
39+
if [ -n "${_REMOTE_USER:-}" ] && [ "${_REMOTE_USER}" != "root" ]; then
40+
# Verify the user exists before using it
41+
if id -u "${_REMOTE_USER}" > /dev/null 2>&1; then
42+
resolved_username="${_REMOTE_USER}"
43+
else
44+
# _REMOTE_USER doesn't exist, fall through to normal detection
45+
resolved_username=""
46+
fi
47+
fi
48+
49+
# If we didn't resolve via _REMOTE_USER, try to find a non-root user
50+
if [ -z "${resolved_username}" ]; then
51+
# Try to find a non-root user from a list of common usernames
52+
# The list includes: devcontainer, vscode, node, codespace, and the user with UID 1000
53+
local possible_users=("devcontainer" "vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '')")
54+
55+
for current_user in "${possible_users[@]}"; do
56+
# Skip empty entries
57+
if [ -z "${current_user}" ]; then
58+
continue
59+
fi
60+
61+
# Check if user exists
62+
if id -u "${current_user}" > /dev/null 2>&1; then
63+
resolved_username="${current_user}"
64+
break
65+
fi
66+
done
67+
68+
# If no user found, use the fallback
69+
if [ -z "${resolved_username}" ]; then
70+
resolved_username="${fallback_user}"
71+
fi
72+
fi
73+
elif [ "${input_username}" = "none" ]; then
74+
# Explicit "none" means use root
75+
resolved_username="root"
76+
else
77+
# Specific username provided - validate it exists
78+
if id -u "${input_username}" > /dev/null 2>&1; then
79+
resolved_username="${input_username}"
80+
else
81+
# User doesn't exist, fall back to root
82+
resolved_username="root"
83+
fi
84+
fi
85+
86+
echo "${resolved_username}"
87+
}

scripts/sync-common-setup.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/bin/bash
2+
#-------------------------------------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See https://github.com/devcontainers/features/blob/main/LICENSE for license information.
5+
#-------------------------------------------------------------------------------------------------------------------------
6+
#
7+
# Script to sync common-setup.sh from the source to all feature _lib directories
8+
# This maintains a single source of truth while deploying to each feature for packaging
9+
#
10+
# Usage: ./scripts/sync-common-setup.sh
11+
#
12+
13+
set -e
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
17+
18+
# The source of truth for common-setup.sh
19+
SOURCE_FILE="${REPO_ROOT}/scripts/lib/common-setup.sh"
20+
21+
# Features that use the common-setup helper
22+
FEATURES=(
23+
"anaconda"
24+
"common-utils"
25+
"conda"
26+
"desktop-lite"
27+
"docker-in-docker"
28+
"docker-outside-of-docker"
29+
"go"
30+
"hugo"
31+
"java"
32+
"kubectl-helm-minikube"
33+
"node"
34+
"oryx"
35+
"php"
36+
"python"
37+
"ruby"
38+
"rust"
39+
"sshd"
40+
)
41+
42+
echo "Syncing common-setup.sh to all features..."
43+
echo "Source: ${SOURCE_FILE}"
44+
echo ""
45+
46+
if [ ! -f "${SOURCE_FILE}" ]; then
47+
echo "Error: Source file not found: ${SOURCE_FILE}"
48+
exit 1
49+
fi
50+
51+
UPDATED_COUNT=0
52+
53+
for feature in "${FEATURES[@]}"; do
54+
TARGET_DIR="${REPO_ROOT}/src/${feature}/_lib"
55+
TARGET_FILE="${TARGET_DIR}/common-setup.sh"
56+
57+
# Create _lib directory if it doesn't exist
58+
mkdir -p "${TARGET_DIR}"
59+
60+
# Copy the file
61+
cp "${SOURCE_FILE}" "${TARGET_FILE}"
62+
63+
echo "✓ Synced to src/${feature}/_lib/common-setup.sh"
64+
UPDATED_COUNT=$((UPDATED_COUNT + 1))
65+
done
66+
67+
echo ""
68+
echo "======================================"
69+
echo "Sync complete!"
70+
echo "Updated ${UPDATED_COUNT} features"
71+
echo "======================================"
72+
echo ""
73+
echo "Note: After running this script, commit the changes:"
74+
echo " git add src/*/lib/common-setup.sh"
75+
echo " git commit -m 'Sync common-setup.sh to all features'"

0 commit comments

Comments
 (0)