mirror of
https://gitlab.com/rensa-nix/direnv.git
synced 2025-12-12 10:00:11 +01:00
chore: initial commit
This commit is contained in:
commit
bada848ca1
2 changed files with 426 additions and 0 deletions
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Rensa Direnv Integration
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
direnv fetchurl https://gitlab.com/rensa-nix/direnv/-/raw/main/direnvrc
|
||||
```
|
||||
|
||||
`.envrc`:
|
||||
```bash
|
||||
source $(fetchurl https://gitlab.com/rensa-nix/direnv/-/raw/main/direnvrc <hash>)
|
||||
|
||||
use envreload //repo/shells/default
|
||||
```
|
||||
|
||||
411
direnvrc
Normal file
411
direnvrc
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# A direnv hook for loading Nix flake-based development environments
|
||||
# with built-in caching and garbage-collection root management.
|
||||
#
|
||||
# Based on
|
||||
# https://github.com/nix-community/nix-direnv
|
||||
# and
|
||||
# https://github.com/paisano-nix/direnv
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
readonly RENSA_DIRENV_VERSION="0.1.0"
|
||||
readonly DIRENV_MIN_VERSION="2.21.3"
|
||||
readonly BASH_MIN_VERSION="4.4"
|
||||
|
||||
#
|
||||
# LOGGING
|
||||
#
|
||||
|
||||
# Usage: __ren_log <LEVEL> "message"
|
||||
# LEVEL can be: TRACE, INFO, WARN, ERROR
|
||||
__ren_log() {
|
||||
local level="$1" msg="$2"
|
||||
local format=$'\E[mren: \E[38;5;8m%s\E[m'
|
||||
case "$level" in
|
||||
TRACE)
|
||||
if [[ -n "${REN_TRACE:-}" ]]; then
|
||||
# shellcheck disable=SC2059
|
||||
log_status "$(printf "$format" "[TRACE] ${msg}")"
|
||||
fi
|
||||
;;
|
||||
INFO)
|
||||
# shellcheck disable=SC2059
|
||||
log_status "$(printf "$format" "${msg}")"
|
||||
;;
|
||||
WARN)
|
||||
# shellcheck disable=SC2059
|
||||
log_error "$(printf "$format" "[WARN] ${msg}")"
|
||||
;;
|
||||
ERROR)
|
||||
# shellcheck disable=SC2059
|
||||
log_error "$(printf "$format" "[ERROR] ${msg}")"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#
|
||||
# UTILS
|
||||
#
|
||||
|
||||
__ren_require_version() {
|
||||
local name="$1" version="$2" required="$3"
|
||||
if ! printf "%s\n%s\n" "$required" "$version" | LC_ALL=C sort --check --version-sort &>/dev/null; then
|
||||
__ren_log ERROR "Minimum required ${name} version is ${required} (found: ${version})."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
__ren_require_cmd_version() {
|
||||
local cmd="$1" required="$2" version
|
||||
if ! has "$cmd"; then
|
||||
__ren_log ERROR "Command not found: $cmd"
|
||||
return 1
|
||||
fi
|
||||
version=$($cmd --version)
|
||||
if [[ $version =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then
|
||||
__ren_require_version "$cmd" "${BASH_REMATCH[1]}" "$required"
|
||||
else
|
||||
__ren_log WARN "Could not parse version for '$cmd'. Skipping version check."
|
||||
fi
|
||||
}
|
||||
|
||||
__cell_path_cache=""
|
||||
__ren_get_cell_path() {
|
||||
if [[ -z "$__cell_path_cache" ]]; then
|
||||
local cell="$1"
|
||||
local cellsFrom
|
||||
cellsFrom=$(__ren_nix eval --raw ".#__std.cellsFrom" 2>/dev/null || true)
|
||||
if [[ -z "$cellsFrom" ]]; then
|
||||
__ren_log TRACE "Could not determine 'cellsFrom' path from flake."
|
||||
return 1
|
||||
fi
|
||||
__cell_path_cache=$cellsFrom
|
||||
fi
|
||||
echo "${REN_ROOT}/${__cell_path_cache}/${cell}"
|
||||
}
|
||||
|
||||
# set in the entrypoint
|
||||
__nix_cmd=""
|
||||
__ren_nix() {
|
||||
"$__nix_cmd" --no-warn-dirty --extra-experimental-features "nix-command flakes" "$@"
|
||||
}
|
||||
|
||||
#
|
||||
# INIT
|
||||
#
|
||||
|
||||
# discovers the project root (git repo) and sets up state directories and variables
|
||||
__ren_init_project() {
|
||||
[[ -n "${REN_ROOT:-}" ]] && return 0
|
||||
local git_repo
|
||||
git_repo=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||
if [[ -z "$git_repo" ]]; then
|
||||
__ren_log ERROR "Not inside a git repository. Cannot determine project root."
|
||||
return 1
|
||||
fi
|
||||
|
||||
export REN_ROOT="$git_repo"
|
||||
export REN_STATE="${REN_ROOT}/.ren"
|
||||
mkdir -p "${REN_STATE}/direnv"
|
||||
# shellcheck disable=SC2034
|
||||
direnv_layout_dir="${REN_STATE}/direnv"
|
||||
}
|
||||
|
||||
#
|
||||
# NIX GCROOTS & ENV STUFF
|
||||
#
|
||||
|
||||
__ren_import_env() {
|
||||
local profile_rc="$1"
|
||||
[[ -f "$profile_rc" ]] || return 0
|
||||
local -A to_restore
|
||||
to_restore=(
|
||||
["NIX_BUILD_TOP"]="${NIX_BUILD_TOP:-__UNSET__}"
|
||||
["TMP"]="${TMP:-__UNSET__}"
|
||||
["TMPDIR"]="${TMPDIR:-__UNSET__}"
|
||||
["TEMP"]="${TEMP:-__UNSET__}"
|
||||
["TEMPDIR"]="${TEMPDIR:-__UNSET__}"
|
||||
["terminfo"]="${terminfo:-__UNSET__}"
|
||||
)
|
||||
local old_xdg_data_dirs="${XDG_DATA_DIRS:-}"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
eval "$(<"$profile_rc")"
|
||||
|
||||
if [[ -n "${NIX_BUILD_TOP+x}" && $NIX_BUILD_TOP == */nix-shell.* && -d $NIX_BUILD_TOP ]]; then rm -rf "$NIX_BUILD_TOP"; fi
|
||||
for key in "${!to_restore[@]}"; do
|
||||
if [[ "${to_restore[$key]}" == "__UNSET__" ]]; then unset "$key"; else export "$key=${to_restore[$key]}"; fi
|
||||
done
|
||||
local new_xdg_data_dirs="${XDG_DATA_DIRS:-}"
|
||||
export XDG_DATA_DIRS=""
|
||||
local IFS=:
|
||||
for dir in $new_xdg_data_dirs${old_xdg_data_dirs:+:}$old_xdg_data_dirs; do
|
||||
dir="${dir%/}"
|
||||
[[ -z "$dir" || ":$XDG_DATA_DIRS:" == *:"$dir":* ]] && continue
|
||||
XDG_DATA_DIRS="${XDG_DATA_DIRS}${XDG_DATA_DIRS:+:}$dir"
|
||||
done
|
||||
}
|
||||
|
||||
__ren_add_gcroot() {
|
||||
local storepath="$1" symlink="$2"
|
||||
__ren_log TRACE "Adding GC root: $symlink -> $storepath"
|
||||
__ren_nix build --out-link "$symlink" "$storepath" >/dev/null
|
||||
}
|
||||
|
||||
__ren_clean_old_gcroots() {
|
||||
__ren_log TRACE "Cleaning old GC roots in $1"
|
||||
rm -rf "${1}/flake-inputs/" "${1}/profile-"*
|
||||
}
|
||||
|
||||
__ren_argsum_suffix() {
|
||||
local out checksum
|
||||
if [[ -n "$1" ]]; then
|
||||
if has sha1sum; then
|
||||
out=$(sha1sum <<<"$1")
|
||||
elif has shasum; then
|
||||
out=$(shasum <<<"$1")
|
||||
else
|
||||
__ren_log WARN "sha1sum/shasum not found; cannot create stable profile name."
|
||||
return
|
||||
fi
|
||||
read -r checksum _ <<<"$out"
|
||||
echo "-$checksum"
|
||||
fi
|
||||
}
|
||||
|
||||
# archive flake inputs and gcroot them
|
||||
__ren_add_flake_input_gcroots() {
|
||||
local flake_dir="$1" __layout_dir="$2"
|
||||
__ren_log TRACE "Archiving flake inputs from '$flake_dir'"
|
||||
|
||||
local flake_inputs_dir="${__layout_dir}/flake-inputs/"
|
||||
mkdir -p "$flake_inputs_dir"
|
||||
|
||||
local flake_inputs_json
|
||||
# run in subshell to not affect the current directory and handle errors gracefully
|
||||
flake_inputs_json=$( (cd "$flake_dir" && __ren_nix flake archive --json --no-write-lock-file -- ".") 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$flake_inputs_json" ]]; then
|
||||
__ren_log TRACE "No inputs found or error archiving flake at '$flake_dir'. Skipping."
|
||||
return
|
||||
fi
|
||||
|
||||
while [[ $flake_inputs_json =~ /nix/store/[^\"]+ ]]; do
|
||||
local store_path="${BASH_REMATCH[0]}"
|
||||
[[ -z "$store_path" ]] && continue
|
||||
__ren_add_gcroot "$store_path" "${flake_inputs_dir}/${store_path##*/}"
|
||||
flake_inputs_json="${flake_inputs_json/${store_path}/}"
|
||||
done
|
||||
}
|
||||
|
||||
# updates gcroots
|
||||
__ren_update_gcroots() {
|
||||
local __layout_dir="$1" tmp_profile="$2" profile="$3" cell="$4"
|
||||
__ren_add_gcroot "$tmp_profile" "$profile"
|
||||
rm -f "$tmp_profile"*
|
||||
# add gcroots for the main flake
|
||||
__ren_add_flake_input_gcroots "$REN_ROOT" "$__layout_dir"
|
||||
|
||||
# if a cell is specified, also gcroot it's flake inputs, if it has a flake
|
||||
if [[ -n "$cell" ]]; then
|
||||
local cell_path
|
||||
if cell_path=$(__ren_get_cell_path "$cell"); then
|
||||
if [[ -f "${cell_path}/flake.nix" ]]; then
|
||||
__ren_log TRACE "Found cell flake. Adding GC roots for inputs from '${cell_path}'."
|
||||
__ren_add_flake_input_gcroots "$cell_path" "$__layout_dir"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# WATCHERS
|
||||
#
|
||||
|
||||
__ren_get_direnv_watches() {
|
||||
local -n _watches_ref=$1
|
||||
[[ -z "${DIRENV_WATCHES-}" ]] && return 0
|
||||
# shellcheck disable=SC2154
|
||||
while IFS= read -r line; do
|
||||
local regex='"[Pp]ath": "(.+)"$'
|
||||
if [[ $line =~ $regex ]]; then
|
||||
local path
|
||||
# shellcheck disable=SC2059
|
||||
path=$(printf "${BASH_REMATCH[1]}")
|
||||
if [[ $path != "${XDG_DATA_HOME:-${HOME:-/var/empty}/.local/share}/direnv/allow/"* ]]; then
|
||||
_watches_ref+=("$path")
|
||||
fi
|
||||
fi
|
||||
done < <("$direnv" show_dump "${DIRENV_WATCHES}")
|
||||
}
|
||||
|
||||
__ren_watch_cell() {
|
||||
local target_spec="$1"
|
||||
read -r cell block _ <<<"${target_spec//\// }"
|
||||
local cell_path
|
||||
if ! cell_path=$(__ren_get_cell_path "$cell"); then
|
||||
__ren_log WARN "Could not determine 'cellsFrom' path from flake. File watching for target files will be incomplete."
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f "${cell_path}/${block}.nix" ]]; then
|
||||
__ren_log TRACE "Watching: ${cell_path}/${block}.nix"
|
||||
watch_file "${cell_path}/${block}.nix"
|
||||
fi
|
||||
if [[ -f "${cell_path}/default.nix" ]]; then
|
||||
__ren_log TRACE "Watching: ${cell_path}/default.nix"
|
||||
watch_file "${cell_path}/default.nix"
|
||||
fi
|
||||
if [[ -d "${cell_path}/${block}" ]]; then
|
||||
__ren_log TRACE "Watching dir: ${cell_path}/${block}"
|
||||
watch_dir "${cell_path}/${block}"
|
||||
fi
|
||||
|
||||
# watch for cell-level flake.nix and flake.lock
|
||||
if [[ -f "${cell_path}/flake.nix" ]]; then
|
||||
__ren_log TRACE "Watching cell flake: ${cell_path}/flake.nix"
|
||||
watch_file "${cell_path}/flake.nix"
|
||||
fi
|
||||
if [[ -f "${cell_path}/flake.lock" ]]; then
|
||||
__ren_log TRACE "Watching cell flake lock: ${cell_path}/flake.lock"
|
||||
watch_file "${cell_path}/flake.lock"
|
||||
fi
|
||||
}
|
||||
|
||||
__ren_setup_watches() {
|
||||
watch_file "$REN_ROOT/flake.nix" "$REN_ROOT/flake.lock"
|
||||
for target in "$@"; do
|
||||
if [[ "$target" != *"#"* ]]; then
|
||||
__ren_watch_cell "$target"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#
|
||||
# BUILD & CACHE
|
||||
#
|
||||
|
||||
# returns 1 if rebuild is needed
|
||||
__ren_is_rebuild_needed() {
|
||||
local profile_rc="$1"
|
||||
if [[ ! -f "$profile_rc" ]]; then
|
||||
__ren_log TRACE "No cached profile found at '$profile_rc'. Rebuilding."
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
local watches=()
|
||||
__ren_get_direnv_watches watches
|
||||
for file in "${watches[@]}"; do
|
||||
if [[ "$file" -nt "$profile_rc" ]]; then
|
||||
__ren_log INFO "Cache invalidated by: $file"
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 0
|
||||
}
|
||||
|
||||
# returns 0 on success, 1 on failure
|
||||
__ren_build_and_cache() {
|
||||
local target_spec="$1" __layout_dir="$2" profile="$3" profile_rc="$4"
|
||||
__ren_log INFO "Building shell from '$target_spec'..."
|
||||
local flake_attr cell=""
|
||||
# allows specifying either flake attr like `./path#devShells.default` or through
|
||||
# ren style `//repo/devShells/default`
|
||||
if [[ "$target_spec" == *"#"* ]]; then
|
||||
flake_attr="$target_spec"
|
||||
__ren_log TRACE "Using direct flake attribute: $flake_attr"
|
||||
else
|
||||
read -r cell block target <<<"${target_spec//\// }"
|
||||
local system
|
||||
system=$(__ren_nix eval --raw --impure --expr builtins.currentSystem)
|
||||
flake_attr=".#${system}.${cell}.${block}.${target}"
|
||||
fi
|
||||
local tmp_profile="${__layout_dir}/tmp-profile.$$"
|
||||
if build_output=$(__ren_nix print-dev-env --profile "$tmp_profile" "$flake_attr"); then
|
||||
__ren_clean_old_gcroots "$__layout_dir"
|
||||
echo "$build_output" >"$profile_rc"
|
||||
__ren_update_gcroots "$__layout_dir" "$tmp_profile" "$profile" "$cell"
|
||||
|
||||
__ren_log TRACE "Cache for '$target_spec' renewed successfully."
|
||||
return 0
|
||||
else
|
||||
rm -f "$tmp_profile"*
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# ENTRYPOINTS
|
||||
#
|
||||
|
||||
# usage in .envrc: use_envreload <target>
|
||||
# <target> can be `//cell/block/target` or a direct flake attr like `.#myShell`
|
||||
use_envreload() {
|
||||
if [[ -z ${REN_SKIP_VERSION_CHECK:-} ]]; then
|
||||
__ren_require_version "bash" "$BASH_VERSION" "$BASH_MIN_VERSION"
|
||||
__ren_require_cmd_version "direnv" "$DIRENV_MIN_VERSION"
|
||||
fi
|
||||
|
||||
if command -v nix &>/dev/null; then
|
||||
__nix_cmd=$(command -v nix)
|
||||
elif [[ -n "${REN_FALLBACK_NIX:-}" ]]; then
|
||||
__nix_cmd="${REN_FALLBACK_NIX}"
|
||||
else
|
||||
__ren_log ERROR "Could not find Nix. Add 'nix' to PATH or set REN_FALLBACK_NIX."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target_spec="${1-}"
|
||||
if [[ -z "$target_spec" ]]; then
|
||||
__ren_log ERROR "use_envreload requires a target argument, e.g., //repo/devShells/default or .#myShell"
|
||||
return 1
|
||||
fi
|
||||
|
||||
__ren_setup_watches "$@"
|
||||
|
||||
local __layout_dir profile profile_rc
|
||||
__layout_dir=$(direnv_layout_dir)
|
||||
profile="${__layout_dir}/profile$(__ren_argsum_suffix "$target_spec")"
|
||||
profile_rc="${profile}.rc"
|
||||
|
||||
if [[ $(__ren_is_rebuild_needed "$profile_rc") -eq 1 ]]; then
|
||||
if ! __ren_build_and_cache "$target_spec" "$__layout_dir" "$profile" "$profile_rc"; then
|
||||
if [[ -f "$profile_rc" ]]; then
|
||||
__ren_log WARN "Nix evaluation failed. Falling back to the last known-good environment."
|
||||
export REN_DID_FALLBACK=1
|
||||
else
|
||||
__ren_log ERROR "Nix evaluation failed, and no cached environment is available."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
__ren_log INFO "Using cached environment for '$target_spec'."
|
||||
fi
|
||||
|
||||
__ren_import_env "$profile_rc"
|
||||
}
|
||||
|
||||
# initialize project environment variables immediately upon sourcing the script,
|
||||
# allows the user to use $REN_STATE etc. in .envrc
|
||||
__ren_init_project
|
||||
__ren_log TRACE "Rensa v${RENSA_DIRENV_VERSION}"
|
||||
|
||||
#
|
||||
# HELPERS
|
||||
# to use in .envrc, eg. to switch between shells etc.
|
||||
#
|
||||
|
||||
# read state from the project
|
||||
read_state() {
|
||||
if [[ -f "$REN_STATE/$1" ]]; then
|
||||
cat "$REN_STATE/$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# set/write state for the project
|
||||
write_state() {
|
||||
echo "$2" >"$REN_STATE/$1"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue