Skip to content
Open
156 changes: 75 additions & 81 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ source=$(dirname $(readlink -f $0))

alias zz_log=$source/src/common-utils/_zz_log.sh

# Track whether this is the root-level invocation to control symlink lifecycle
_install_root="${INSTALL_ROOT_CALL:-1}"
export INSTALL_ROOT_CALL=0

# Prepare for local installation by creating a temporary directory and linking common utils
find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do
ln -sf $file $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//')
done
export PATH=$PATH:$source/src/common-utils
if [ "$_install_root" = "1" ]; then
find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do
ln -sf $file $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//')
done
export PATH=$PATH:$source/src/common-utils
fi

# Load arguments for the script
eval $(
Expand All @@ -27,27 +33,50 @@ help

zz_log i "Installing devcontainer/features"

# Extract tomgrv devcontainer features listed in a devcontainer.json file
devcontainer_features() {
_file="$1"
[ -f "$_file" ] || return 0
sed '/^\s*\/\//d' "$_file" | \
jq -r '.features // {} | to_entries[] |
select(.key | contains("tomgrv/devcontainer-features")) |
.key | split("/")[-1] | split(":")[0]' 2>/dev/null
}

# Find devcontainer.json files in standard locations and extract features
find_devcontainer_features() {
_search_dir="${1:-.}"

# Check standard devcontainer file locations in priority order
for _f in \
"$_search_dir/.devcontainer/devcontainer.json" \
"$_search_dir/devcontainer.json" \
"$_search_dir/.devcontainer.json"; do
if [ -f "$_f" ]; then
devcontainer_features "$_f"
return 0
fi
done

# Check .devcontainer/<folder>/devcontainer.json for multiple configurations
_found=$(find "$_search_dir/.devcontainer" -maxdepth 2 -mindepth 2 -name "devcontainer.json" 2>/dev/null | head -1)
if [ -n "$_found" ]; then
devcontainer_features "$_found"
fi
}

# If 'all' argument is provided, set stubs and features to install all default features
if [ -n "$all" ]; then
echo "${Yellow}Add default features${End}"
stubs=1
features=$(sed '/^\s*\/\//d' $source/stubs/.devcontainer/devcontainer.json | jq -r '.features | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key|
split("/")[-1] | split(":")[0]')
features=$(find_devcontainer_features "$source/stubs")
fi

# If 'upd' argument is provided, set stubs and features to update all features
if [ -n "$upd" ]; then
echo "${Green}Update features${End}"
stubs=1
features=$(
sed '/^\s*\/\//d' .devcontainer/devcontainer.json |
jq -r '.features | to_entries[] |
select(.key | contains("tomgrv/devcontainer-features")) |
.key |
split("/")[-1] |
split(":")[0]' |
tr '\n' ' '
)
features=$(find_devcontainer_features "." | tr '\n' ' ')
fi

# If 'package' argument is provided, use the specified package.json file
Expand All @@ -62,86 +91,51 @@ if [ -n "$package" ]; then

# Extract features from the package.json file if not already set
if [ -z "$features" ]; then
features=$(cat $file | jq -r '.devcontainer.features | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key| split("/")[-1] | split(":")[0]')
features=$(jq -r '.devcontainer.features // {} | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key| split("/")[-1] | split(":")[0]' "$file")
fi
fi

# If no features specified so far, auto-detect from devcontainer files in current directory
if [ -z "$features" ] && [ -z "$stubs" ] && [ -z "$all" ]; then
detected=$(find_devcontainer_features "." | tr '\n' ' ')
if [ -n "$detected" ]; then
features="$detected"
echo "${Green}Detected features from devcontainer files: $features${End}"
fi
fi

# Resolve transitive dependencies and expand the feature list in topological order
# using install-deps as the dedicated resolver
if [ -n "$features" ]; then
features=$(install-deps "$source" $features | tr '\n' ' ')
fi

# Merge all files from the stub folder to the root using git merge-file if stubs are selected
if [ -n "$stubs" ]; then
echo "${Yellow}Installing stubs from ${UYellow}$source/src/common-utils/${Yellow}...${End}"
$source/src/common-utils/_configure-feature.sh -s $source .
echo "${Green}Stubs installed${End}"
fi

# If features are selected, proceed with installation
# If features are selected, delegate installation of each to install-feat
if [ -n "$features" ]; then

echo "${Green}Selected features: $features${End}"

# Create an alias for the _install-feature.sh script
alias install-feature=$(dirname $0)/src/common-utils/_install-feature.sh

# Check if the script is running inside a container
if [ "$CODESPACES" != "true" ] && [ "$REMOTE_CONTAINERS" != "true" ] && [ -z "$DEV_CONTAINER_FILE_PATH" ]; then

echo "${Red}You are not in a container${End}"

# Run the install.sh script for each selected feature
for feature in $features; do
if [ -f "$source/src/$feature/install.sh" ]; then
echo "${Yellow}Running src/$feature/install.sh...${End}"
if sh $source/src/$feature/install.sh; then
echo "${Green}$feature installed${End}"
else
echo "${Red}$feature installation failed${End}"
exit 1
fi
else
echo "${Red}$feature not found${End}"
exit 1
fi
done

# Run the configure.sh script for each selected feature
for feature in $features; do
featureSource=""
if [ -d "/tmp/$feature" ]; then
featureSource="/tmp/$feature"
elif [ -d "/usr/local/share/$feature" ]; then
featureSource="/usr/local/share/$feature"
fi

if [ -n "$featureSource" ]; then
echo "${Yellow}Configuring $featureSource...${End}"
sh $source/src/common-utils/_configure-feature.sh -s $featureSource $feature
echo "${Green}$feature configured${End}"
else
echo "${Red}$feature not found${End}"
exit 1
fi
done

elif [ -n "$stubs" ]; then

# stubs are selected, configure stubs of the selected features
for feature in $features; do
echo "${Yellow}Deploying stubs for $feature...${End}"
$source/src/common-utils/_configure-feature.sh -s $source/src/$feature $feature
echo "${Green}Stubs for $feature deployed${End}"
done

else
# If inside a container, suggest using devutils as devcontainer features
echo "${Purple}You are in a container: use as devcontainer features:${End}"
for feature in $features; do
echo "${Purple}ghcr.io/tomgrv/devcontainer-features/$feature${End}"
done
for feature in $features; do
if [ -n "$stubs" ]; then
install-feat "$source" "$feature" --stubs
else
install-feat "$source" "$feature"
fi
done

fi
fi

# Remoce all links to common utils
echo "Remove temp files..."
find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do
rm $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//')
done
# Remove all links to common utils (only at root-level invocation)
if [ "$_install_root" = "1" ]; then
echo "Remove temp files..."
find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do
rm $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//')
done
fi
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"url": "https://buymeacoffee.com/tomgrv"
},
"scripts": {
"install": "sh ./install.sh -s",
Comment thread
tomgrv marked this conversation as resolved.
"lint": "npx --yes lint-staged",
"release": "commit-and-tag-version --no-verify --",
"test": "echo \"Warning: no test specified\"",
Expand Down
2 changes: 2 additions & 0 deletions src/act/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"act-run": "./_act-run.sh"
},
"scripts": {
"install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi",
"postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . act; fi",
"install-act": "./install-act.sh"
},
"peerDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions src/common-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ The feature also includes the following VS Code customizations:
- Installs specified common utilities such as jq and dos2unix.
- Supports specifying additional utilities to install.

## Install internals

- `_install-bin.sh` installs links for common-utils scripts in a writable bin directory.
- `_select-bin.sh` resolves the writable bin directory with this fallback order:
1. `INSTALL_BIN_DIR` (or `/usr/local/bin` by default)
2. `~/.local/bin`
3. First writable directory found in `$PATH` (excluding `node_modules/.bin`)
4. `<feature-target>/bin`

## Additional utilities

In addition to the specified utilities, some additional local utilities are also provided:
Expand Down
24 changes: 19 additions & 5 deletions src/common-utils/_install-bin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,23 @@ fi

zz_log i "Installing bin scripts for {Purple $feature}..."

# Find all shell scripts in the target directory, make them executable, and create symbolic links in /usr/local/bin
find $target -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do
# Create a symbolic link in /usr/local/bin with the script name (without the leading underscore and .sh extension)
link=/usr/local/bin/$(basename $file | sed 's/^_//;s/.sh$//')
ln -sf $file $link && zz_log s "Linked {U $file} to {U $link}"
link_dir="$(sh "$(dirname "$0")/_select-bin.sh" "$target")"
[ -n "$link_dir" ] || {
zz_log e "No writable bin directory found"
exit 1
}

case ":$PATH:" in
*":$link_dir:"*) ;;
*)
export PATH="$link_dir:$PATH"
zz_log w "Added {U $link_dir} to PATH for current install session"
;;
esac

# Find all shell scripts in the target directory, make them executable, and create symbolic links in the selected bin directory
find "$target" -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while IFS= read -r file; do
# Create a symbolic link in the selected bin directory with the script name (without the leading underscore and .sh extension)
link="$link_dir/$(basename "$file" | sed 's/^_//;s/.sh$//')"
ln -sf "$file" "$link" && zz_log s "Linked {U $file} to {U $link}"
done
78 changes: 78 additions & 0 deletions src/common-utils/_install-deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/sh

# Resolves transitive tomgrv/devcontainer-features dependencies from devcontainer-feature.json files.
# Usage: install-deps <source_dir> <feature> [<feature>...]
# Outputs the complete dependency list in topological order (deps before dependents), one per line.

_source="$1"
shift

if [ -z "$_source" ]; then
echo "Usage: install-deps <source_dir> <feature>..." >&2
exit 1
fi

features="$*"
[ -z "$features" ] && exit 0

# Extract direct tomgrv/devcontainer-features dependencies of a single feature
_feature_deps() {
_manifest="$_source/src/$1/devcontainer-feature.json"
[ -f "$_manifest" ] || return 0
jq -r '.dependsOn // {} | to_entries[] |
select(.key | contains("tomgrv/devcontainer-features")) |
.key | split("/")[-1] | split(":")[0]' "$_manifest" 2>/dev/null
}

# BFS: collect the full set of reachable features (input + all transitive deps)
_all=""
_queue="$features"
while [ -n "$_queue" ]; do
_next=""
for _f in $_queue; do
case " $_all " in
*" $_f "*) continue ;;
esac
_all="$_all $_f"
for _dep in $(_feature_deps "$_f"); do
case " $_all $_next " in
*" $_dep "*) ;;
*) _next="$_next $_dep" ;;
esac
done
done
_queue="$_next"
done

# Topological sort: emit features whose tomgrv deps are all already emitted
_emitted=""
_pending="$_all"
_changed=1
while [ "$_changed" = "1" ] && [ -n "$_pending" ]; do
_changed=0
_still_pending=""
for _f in $_pending; do
_deps_ok=1
for _dep in $(_feature_deps "$_f"); do
case " $_emitted " in
*" $_dep "*) ;;
*)
_deps_ok=0
break
;;
esac
done
if [ "$_deps_ok" = "1" ]; then
_emitted="$_emitted $_f"
_changed=1
else
_still_pending="$_still_pending $_f"
fi
done
_pending="$_still_pending"
done

# Append any remaining features (handles cycles or missing manifests)
_emitted="$_emitted $_pending"

echo "$_emitted" | tr ' ' '\n' | grep -v '^$'
Loading