devenv
Summary
- Implementation language: C++
- Runtime requirements: An interactive shell
- Base command:
umbra devenv,umbra env - Command run without arguments:
umbra env default - State: alpha
umbra devenvis a utility command for loading and managing repo-specific configurations and environments.
Limitations
Will not work great in environments where POSIX-compliant and non-POSIX compliant shells are mixed, due to .shell.devenv being shared across shells. The only way to make this work portably without requiring multiple implementations is to standardise on POSIX-compliant shells, or standardise on specific non-compliant shells.
Umbra does not enforce either approach.
Additionally, only a few shells are supported. This is due to another set of hacks required to actually get the extra commands into the shell without requiring adding more stuff to your init files. This means the only supported shells are:
- bash
- zsh
This is due to a major limitation in shells, where there is no way to run -ic and have the command run first, and then get dropped into a real interactive shell.
Core functionality
Shell files
Shell files (.shell.devenv) are standard shell files that add functionality to the shell. These are sourced by the shell, equivalently to a .zshrc or a .bashrc. By default, the user's standard shell is used, so the .shell.devenv should be written with common syntax.
Public .shell.devenv SHOULD be posix-compatible, as other people with other shells may run it. Private .shell.devenv files can be anything you want, as long as you're able to run it.
Shell-specific .shell.devenvs will be the form .{shell name}.devenv, for example .bash.devenv, based on (pseudocode) $SHELL.split("/")[-1].tolower(). If a shell-specific .devenv file exists, it takes priority over .shell.devenv. However, these are not implemented, and won't be for the foreseeable future. If you have a need for this feature, open an issue (if it doesn't already exist), and it'll be moved up the list of priorities.
Special available environment variables
Umbra sets some special environment variables that can be used within the scripts to control behaviour. These are:
UMBRA_DEVENV_ENVIRONMENT: defaults todefault, corresponds to an environment in the.env.devenvfile corresponding to the scope of the file. Can be used to control how to set dynamic variables that can't be set in an environment file due to computational requirements.UMBRA_DEVENV_SCOPE_PATH: The scope path pointing to the current directory containing the sourced file. Can be used to construct paths to other scripts to source.
Technical: How shell setup works
To be more explicit about the details in from the limtations section, not a single shell supports -ic where the shell actually goes interactive after the command executes. As a result, an injection system had to be set up. This is done per shell. However, in all cases, the .shell.devenv is sourced AFTER all other shell files.
The .*rc files can be found in the shell/wrangler directory if you're interested in per-shell details.
Very technical details
Environment files
Environment files (.env.devenv) can be used to control the details of what the shell files do. If you want to provide several configurations of a shell file, that's done with an environment.
These files are optional, as their main functionality is adding environment arguments.
In an environment configuration, you can set specific shell variables that you can use to control what the shell files do. Note that these variables are not enforced by umbra: you set whatever you need to make logical scripts, and need to keep track of these yourself.
This is with one exception: UMBRA_DEVENV_ENVIRONMENT, which is set by umbra prior to shell execution. This always matches an environment declared in the .env.devenv file.
The environment files are JSON files with umbra template support, but no code execution. All variable contents support templates, but variable names cannot be templated. For any dynamic variables you need, you'll need to compute and set them in the .shell.devenv file. For information about templating, see templating
Syntax and example:
{
// This file is a modified variant of jsonc, where only // comments are allowed, and
// they must be alone on a line. This is a limitation of the JSON parser used, and json comments
// being a preprocessor step.
"envs": {
"default": {
// The default environment always exists, and is (shock) the default environment.
// It's the environment that's loaded if no environment is specified.
// If you'd like, you can alias this environment to another environment:
"alias-for": "test"
// otherwise, all the same keys are supported as in other environments.
},
// The following environment names are not special in any way, they're just used to demonstrate
// how umbra can work in a two-environment deployment system.
// You can name these whatever you want, as long as it's valid YAML
"test": {
// Vars defines any environment variables you want included. These are not allowed
// to start with UMBRA_, as that is an umbra-reserved prefix. UMBRA_-variables are
// protected within these files and cannot be touched. Aside that, all variables
// are fair game.
// These override any existing environment variables, so make sure what you're overriding
// isn't critical to basic use of the shell (such as $PATH).
"vars": {
"VAULT_ADDR": "https://test.<redacted>"
},
// Similar to vars, but prepends instead of modifies. It's assumed the variables are
// colon-separated arrays.
"prepend": {
"PATH": [
"/usr/local/bin"
]
}
},
}
}
Environment files are always sourced before shell files, but don't have to be used with shell files. You can have a standalone environment file.
prepend-limitations with PATH, and setup order for environment variables
While PATH is used in the above example, the injection isn't guaranteed to ensure that your additions are the first PATH components. This is a consequence of the environment variables being sourced before the shell's built-in config files, so any configurations in the user's .*rc files can shadow the additions made in the .env.devenv.
I'd like to change this at some point, but I can't agree with myself on the syntax, so I'm deprioritising it for now. in the meanwhile, you can specify an environment variable, for example[^1]:
"vars": {
"PATH_PREPEND": "{{git_root}}/build/bin"
}
And then, in your .shell.devenv, you modify the path:
export PATH="${PATH_PREPEND}:${PATH}"
As noted in the section for the .shell.devenv file, this works because .shell.devenv is sourced after the shell .*rc files.
Lookup order and merging strategy
By default, the lookup order is:
UMBRA_DEVENV_PRIVATE_SUBDIR(default:{{git_root}}/.git/devenv; templates supported)UMBRA_DEVENV_PUBLIC_SUBDIR(default:{{git_root}}/dev/devenv; templates supported)
Only one shell file is sourced. This is a feature meant to allow the shell files to contain semi-arbitrary startup logic that can be overridden in its entirety by adding a separate file. This will be changed in the future to allow both to be sourced and optionally picked if one of the two should be disabled, but this requires further ✨ magic ✨ research, as I don't yet know how this can be done compactly and with as little friction as possible.
For the env files, the lookup order is reversed, as all the files are included and merged through the resulting actions overriding previous actions. This avoids needing cursed merging algorithm shit, at the expense of some (but relatively speaking, extremely little) wasted compute.
Alias priority
As shown in the configuration syntax, the default environment can have an alias-for key to point it to another environment. However, there are two conflicting scenarios:
- Either the public or private file aliases, while the other defines it to something special
- Both the files alias them to different environments
Both of these are handled identically: the private file takes priority, and the other is disregarded.
If the private file declares that default is an alias, it's aliased. If the private file declares it's a full environment, it's a full environment.
Security
This module goes through all potential files, and sources them all. This means there's attack vectors going via the files.
To deal with this, devenv has the following security strategy:
- No files are sourced without permission. There is a
--trustedflag to override this, but using it is discouraged. -
As long as
--trustedisn't passed (meaning by default), all scripts have to be manually approved in one of two ways:- Interactively, as long as
--no-interactiveisn't passed - Manually, using
umbra devenv audit
Scripts can be automatically approved, but this is only recommended for scripts from private repositories you fully control. * Approval is only valid for the current state of the file, unless it's default-approved. If it changes, the approval is invalidated and a new approval is required.
- Interactively, as long as
[!warning] Umbra only addresses the first level, and does not scan for other imported files. If the shell script
sources a file that's then changed, reapproval is not triggered. If you're running scripts from sources so untrusted you're not sure if you can trust hops from your main shell file, you probably need to reconsider your security model.
Supported input environment variables
This list describes the variables devenv uses for input.
SHELL(default: none; must be set by the shell): used to determine what shell to spawn. This is also a standard variable set in many (all?) shells, so it's recommended you override it per command rather than with anexport, for exampleSHELL=$(which bash) umbra devenv. It's recommended to use the--shellflag instead, which doesn't require a path:umbra devenv -s bashUMBRA_DEVENV_PRIVATE_SUBDIR(default:{{git_root}}/.git/devenv; templates supported): see lookup order and merging strategyUMBRA_DEVENV_PUBLIC_SUBDIR(default:{{git_root}}/dev/devenv; templates supported): see lookup order and merging strategy
Examples
p10k integration
There's no built-in support for umbra, but you can always add your own. In your .p10k.zsh:
function prompt_umbra() {
if [[ "$UMBRA_DEVENV_ENVIRONMENT" == "" ]]; then
return
fi
p10k segment -t "umbra:$UMBRA_DEVENV_ENVIRONMENT" -b purple
}
...
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
# ...
umbra
# ...
)