Skip to content

Commit

Permalink
feat: support extending a parent environment
Browse files Browse the repository at this point in the history
Adds a new environment configuration to bin/hermit.hcl
```
inherit-parent = true
```

Setting this inherits the Hermit environment from a parent directory, inheriting environment variables and packages.

This should be useful in larger monorepos where we want to have a single root environment with most tooling, but want to still allow subprojects to have their own owerwrites and environment variables.

Note, the packages in parent environment won't have access to the child environment even when being called from the child environment. This means that any package calling another package in the parent environment will still call the parent package even if that is overwritten in the child environment. Though this can be potentially confusing, it should give more consistent behaviour between parent and child environments. Also, package dependencies can not cross environments. So, it is not possible to have a package dependency from a child environment package to a parent environment package.

Finally, for the time being, all hermit operations apply only to the currently active environment. So, it is not possible to upgrade or uninstall parent packages from the child environment, for example
  • Loading branch information
jvmakine committed Mar 13, 2024
1 parent 4a6b052 commit 111d84c
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 10 deletions.
11 changes: 6 additions & 5 deletions docs/docs/usage/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ customise that Hermit environment.

## Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `env` | `{string:string}?` | Extra environment variables. |
| `sources` | `[string]?` | Package manifest sources in order of preference. |
| `manage-git` | `bool?` | Whether Hermit should manage Git. |
| Attribute | Type | Description |
|-----------|------|---------------------------------------------------------------------------------------|
| `env` | `{string:string}?` | Extra environment variables. |
| `sources` | `[string]?` | Package manifest sources in order of preference. |
| `manage-git` | `bool?` | Whether Hermit should manage Git. |
| `inherit-parent` | `bool?` | Whether this Hermit environment should inherit an environment from a parent diectory. |

## Per-environment Sources

Expand Down
48 changes: 43 additions & 5 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ const (

// Config for a Hermit environment.
type Config struct {
Envars envars.Envars `hcl:"env,optional" help:"Extra environment variables."`
Sources []string `hcl:"sources,optional" help:"Package manifest sources."`
ManageGit bool `hcl:"manage-git,optional" default:"true" help:"Whether Hermit should automatically 'git add' new packages."`
AddIJPlugin bool `hcl:"idea,optional" default:"false" help:"Whether Hermit should automatically add the IntelliJ IDEA plugin."`
Envars envars.Envars `hcl:"env,optional" help:"Extra environment variables."`
Sources []string `hcl:"sources,optional" help:"Package manifest sources."`
ManageGit bool `hcl:"manage-git,optional" default:"true" help:"Whether Hermit should automatically 'git add' new packages."`
InheritParent bool `hcl:"inherit-parent,optional" default:"false" help:"Whether this environment inherits a potential parent environment from one of the parent directories"`
AddIJPlugin bool `hcl:"idea,optional" default:"false" help:"Whether Hermit should automatically add the IntelliJ IDEA plugin."`
}

// Env is a Hermit environment.
Expand Down Expand Up @@ -974,11 +975,27 @@ func (e *Env) Envars(l *ui.UI, inherit bool) ([]string, error) {
// environment variables defined in the packages installed in the environment,
// and finally any environment variables explicitly configured in the environment.
func (e *Env) EnvOps(l *ui.UI) (envars.Ops, error) {
var ops envars.Ops

if e.config.InheritParent {
// load EnvOps from a potential parent environment first
parent, err := e.openParent()
if err != nil {
return nil, err
}
if parent != nil {
ops, err = parent.EnvOps(l)
if err != nil {
return nil, err
}
}
}

pkgs, err := e.ListInstalled(l)
if err != nil {
return nil, err
}
return e.allEnvarOpsForPackages(nil, nil, pkgs...), nil
return append(ops, e.allEnvarOpsForPackages(nil, nil, pkgs...)...), nil
}

// SetEnv sets an extra environment variable.
Expand Down Expand Up @@ -1433,6 +1450,27 @@ func (e *Env) resolver(l *ui.UI) (*manifest.Resolver, error) {
return resolver, nil
}

// openParent finds the closest hermit Env from the parent directories of this Env.
// if no such environment was found, returns nil
func (e *Env) openParent() (*Env, error) {
path, err := filepath.Abs(e.envDir)
if err != nil {
return nil, err
}
for {
path = filepath.Dir(path)
if path == "/" {
// we are at the root of the filesystem, no hermit environments found
return nil, nil
}
if _, err := os.Stat(filepath.Join(path, "bin", "activate-hermit")); err == nil {
return OpenEnv(path, e.state, e.packageSource, nil, e.httpClient, e.scriptSums)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
}
}

// tidySha256Db is a helper function to remove leading and trailing
// whitespaces and empty lines from a multiline string containing
// SHA-256 hash digests, and return a slice of digest strings that do
Expand Down
16 changes: 16 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,22 @@ func TestIntegration(t *testing.T) {
. env2/bin/activate-hermit
assert test "$FOO" = "BAR"
`},
{name: "InheritsEnvVariables",
preparations: prep{fixture("environment_inheritance"), activate("child_environment")},
script: `echo "${OVERWRITTEN} - ${NOT_OVERWRITTEN}"`,
expectations: exp{outputContains("child - parent")},
},
{name: "InheritanceOverwritesBinaries",
preparations: prep{fixture("environment_inheritance"), activate(".")},
script: `
hermit install binary
. child_environment/bin/activate-hermit
assert test "$(binary.sh)" = "Running from parent"
hermit install binary
assert test "$(binary.sh)" = "Running from child"
`,
},
}

checkForShells(t)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Hermit environment

This is a [Hermit](https://github.com/cashapp/hermit) bin directory.

The symlinks in this directory are managed by Hermit and will automatically
download and install Hermit itself as well as packages. These packages are
local to this environment.
21 changes: 21 additions & 0 deletions integration/testdata/environment_inheritance/bin/activate-hermit
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# This file must be used with "source bin/activate-hermit" from bash or zsh.
# You cannot run it directly
#
# THIS FILE IS GENERATED; DO NOT MODIFY

if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi

BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
if "${BIN_DIR}/hermit" noop > /dev/null; then
eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"

if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
hash -r 2>/dev/null
fi

echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
fi
43 changes: 43 additions & 0 deletions integration/testdata/environment_inheritance/bin/hermit
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
#
# THIS FILE IS GENERATED; DO NOT MODIFY

set -eo pipefail

export HERMIT_USER_HOME=~

if [ -z "${HERMIT_STATE_DIR}" ]; then
case "$(uname -s)" in
Darwin)
export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
;;
Linux)
export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
;;
esac
fi

export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
export HERMIT_CHANNEL
export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}

if [ ! -x "${HERMIT_EXE}" ]; then
echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
INSTALL_SCRIPT="$(mktemp)"
# This value must match that of the install script
INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38"
if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
else
# Install script is versioned by its sha256sum value
curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
# Verify install script's sha256sum
openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
'$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
fi
/bin/bash "${INSTALL_SCRIPT}" 1>&2
fi

exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
2 changes: 2 additions & 0 deletions integration/testdata/environment_inheritance/bin/hermit.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sources = ["env:///packages"]
env = { OVERWRITTEN:parent, NOT_OVERWRITTEN:parent }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Hermit environment

This is a [Hermit](https://github.com/cashapp/hermit) bin directory.

The symlinks in this directory are managed by Hermit and will automatically
download and install Hermit itself as well as packages. These packages are
local to this environment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# This file must be used with "source bin/activate-hermit" from bash or zsh.
# You cannot run it directly
#
# THIS FILE IS GENERATED; DO NOT MODIFY

if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi

BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
if "${BIN_DIR}/hermit" noop > /dev/null; then
eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"

if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
hash -r 2>/dev/null
fi

echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
#
# THIS FILE IS GENERATED; DO NOT MODIFY

set -eo pipefail

export HERMIT_USER_HOME=~

if [ -z "${HERMIT_STATE_DIR}" ]; then
case "$(uname -s)" in
Darwin)
export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
;;
Linux)
export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
;;
esac
fi

export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
export HERMIT_CHANNEL
export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}

if [ ! -x "${HERMIT_EXE}" ]; then
echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
INSTALL_SCRIPT="$(mktemp)"
# This value must match that of the install script
INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38"
if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
else
# Install script is versioned by its sha256sum value
curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
# Verify install script's sha256sum
openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
'$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
fi
/bin/bash "${INSTALL_SCRIPT}" 1>&2
fi

exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sources = ["env:///packages"]
inherit-parent = true
env = { OVERWRITTEN:child }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description = "binary"
source = "${env}/packages/binary.sh"
binaries = ["binary.sh"]
version "1.0.1" {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Running from child"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description = "binary"
source = "${env}/packages/binary.sh"
binaries = ["binary.sh"]
version "1.0.0" {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Running from parent"

0 comments on commit 111d84c

Please sign in to comment.