Initial commit

This commit is contained in:
Bryan Bennett 2023-12-21 08:51:20 -05:00
commit 69aedbe308
No known key found for this signature in database
GPG key ID: EE149E4215408DE9
10 changed files with 576 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Bryan Bennett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# flake_env
Yet another direnv plugin for flakes.
This one is kind of experimental.
## Why not `nix-direnv`?
I am one of the core contributors on nix_direnv, but wanted to try a new approach.
nix-direnv got held up a bit by depending on differing behaviors of external programs not shipped with bash
(or things that *are* shipped with bash, but a differing implementation ended up in front of the bash version in PATH).
This is an attempt to simplify a bit.
I ported the nix-direnv `use_flake` function to ReasonML.
Implementing most things by hand in ReasonML is pretty simple and (more importantly) portable.
This removes *most* of the dependencies besides bash and nix.
The bash that exists is pretty portable between versions, since it only does environment manipulation.
## Installation & Usage
### Installation
These details are evolving.
I'll make sure to update this document when things change.
For now, there is no NixOS, nix-darwin, or home-manager module that points at this tool.
The only thing you can really do is use this repo's flake as an input.
Then you'll probably need to source `${flake_env}/share/flake_env/direnvrc`.
You should be able to do that with:
* `programs.direnv.direnvrcExtra` on NixOS
* `programs.direnv.stdlib` if using home-manager
### Usage
Example `.envrc`:
```sh
watch_file **/*.nix
use flake_env .
```
## Credits
This takes huge inspiration (and literal code-chunks) from nix-direnv.
Thanks to Mic92 for nix-direnv and kingarrrt for their recent contributions.

44
default.nix Normal file
View file

@ -0,0 +1,44 @@
{ buildDunePackage
, lib
, core
, core_unix
, findlib
, ocaml
, ppx_yojson_conv
, ppx_yojson_conv_lib
, re
, reason
, sha
}:
buildDunePackage {
pname = "flake_env";
version = "0.1";
src = ./.;
duneVersion = "3";
postPatch = ''
substituteInPlace --replace "flake_env" "$out/bin/flake_env" direnvrc
'';
postInstall = ''
install -m400 -D direnvrc $out/share/flake_env/direnvrc
'';
nativeBuildInputs = [
reason
];
propagatedBuildInputs = [
core
core_unix
findlib
ocaml
ppx_yojson_conv
ppx_yojson_conv_lib
re
sha
];
meta = with lib; {
description = "Yet another flake plugin for direnv";
homepage = "https://git.sr.ht/~bryan_bennett/flake_env";
license = licenses.mit;
platforms = platforms.unix;
};
}

68
direnvrc Normal file
View file

@ -0,0 +1,68 @@
# The below code is taken largely from nix-direnv.
# nix-direnv's license is replicated below:
#
# Copyright (c) 2019 Nix community projects
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
_nix_export_or_unset() {
local key=$1 value=$2
if [[ $value == __UNSET__ ]]; then
unset "$key"
else
export "$key=$value"
fi
}
use_flake_env() {
local old_nix_build_top=${NIX_BUILD_TOP:-__UNSET__}
local old_tmp=${TMP:-__UNSET__}
local old_tmpdir=${TMPDIR:-__UNSET__}
local old_temp=${TEMP:-__UNSET__}
local old_tempdir=${TEMPDIR:-__UNSET__}
local old_xdg_data_dirs=${XDG_DATA_DIRS:-}
local ld=$(direnv_layout_dir)
eval $(flake_env "$1" "$ld")
# `nix print-dev-env` will create a temporary directory and use it as TMPDIR
# We cannot rely on this directory being available at all times,
# as it may be garbage collected.
# Instead - just remove it immediately.
# Use recursive & force as it may not be empty.
if [[ -n ${NIX_BUILD_TOP+x} && $NIX_BUILD_TOP == */nix-shell.* && -d $NIX_BUILD_TOP ]]; then
rm -rf "$NIX_BUILD_TOP"
fi
_nix_export_or_unset NIX_BUILD_TOP "$old_nix_build_top"
_nix_export_or_unset TMP "$old_tmp"
_nix_export_or_unset TMPDIR "$old_tmpdir"
_nix_export_or_unset TEMP "$old_temp"
_nix_export_or_unset TEMPDIR "$old_tempdir"
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%/}" # remove trailing slashes
if [[ :$XDG_DATA_DIRS: == *:$dir:* ]]; then
continue # already present, skip
fi
XDG_DATA_DIRS="$XDG_DATA_DIRS${XDG_DATA_DIRS:+:}$dir"
done
}

24
dune-project Normal file
View file

@ -0,0 +1,24 @@
(lang dune 3.11)
(name flake_env)
(generate_opam_files true)
(source
(sourcehut bryan_bennett/flake_env))
(authors "Bryan Bennett")
(maintainers "Bryan Bennett")
(license MIT)
(documentation https://git.sr.ht/~bryan_bennett/flake_env)
(package
(name flake_env)
(allow_empty)
(synopsis "Yet another flake plugin for direnv")
(depends ocaml dune)
(tags
("direnv" "nix" "flake")))

48
flake.lock generated Normal file
View file

@ -0,0 +1,48 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1701473968,
"narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1702272962,
"narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

44
flake.nix Normal file
View file

@ -0,0 +1,44 @@
{
description = "Yet another flake plugin for direnv";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
};
outputs = inputs @ { flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; }
({ lib, ... }: {
systems = [
"aarch64-linux"
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem = { config, pkgs, self', ... }: {
packages = {
flake_env = pkgs.ocamlPackages.callPackage ./default.nix { };
default = config.packages.flake_env;
};
devShells.default = pkgs.mkShell {
inputsFrom = [ self'.packages.default ];
packages = [
pkgs.ocamlPackages.dune_3
pkgs.ocamlPackages.findlib
pkgs.ocamlPackages.ocaml
pkgs.ocamlPackages.ocaml-lsp
pkgs.ocamlPackages.ocamlformat
pkgs.ocamlPackages.ocamlformat-rpc-lib
];
};
};
flake = {
overlays.default = final: _prev: {
flake_env = final.callPackage ./default.nix { };
};
};
});
}

30
flake_env.opam Normal file
View file

@ -0,0 +1,30 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "Yet another flake plugin for direnv"
maintainer: ["Bryan Bennett"]
authors: ["Bryan Bennett"]
license: "MIT"
tags: ["direnv" "nix" "flake"]
homepage: "https://sr.ht/~bryan_bennett/flake_env"
doc: "https://git.sr.ht/~bryan_bennett/flake_env"
bug-reports: "https://todo.sr.ht/~bryan_bennett/flake_env"
depends: [
"ocaml"
"dune" {>= "3.11"}
"odoc" {with-doc}
]
build: [
["dune" "subst"] {dev}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://git.sr.ht/~bryan_bennett/flake_env"

5
src/dune Normal file
View file

@ -0,0 +1,5 @@
(executable
(name flake_env)
(public_name flake_env)
(libraries core core_unix core_unix.filename_unix core_unix.sys_unix ppx_yojson_conv re sha)
(preprocess (pps ppx_yojson_conv ppx_jane)))

245
src/flake_env.re Normal file
View file

@ -0,0 +1,245 @@
open Core;
module Unix = Core_unix;
type version_number = {
major: int,
minor: int,
point: int,
};
[@deriving yojson]
type watch = {
exists: bool,
modtime: int,
path: string
};
[@deriving yojson]
type watches = array<watch>;
let required_direnv_version = {
major: 2,
minor: 21,
point: 3
};
let required_nix_version = {
major: 2,
minor: 10,
point: 0
};
// TODO: test this
let compare_version_number = (a, b) => {
switch (a, b) {
| (a, b) when a.major == b.major && a.minor == b.minor && a.point == b.point => 0
| (a, b) when a.major < b.major => -1
| (a, b) when a.major == b.major && a.minor < b.minor => -1
| (a, b) when a.major == b.major && a.minor == b.minor && a.point < b.point => -1
| _ => 1
}
}
let semver_re = Re.compile(Re.Posix.re({|([0-9]+)\.([0-9]+)\.([0-9]+)|}));
let extract_version_number = (cmd) => {
let full_cmd = cmd ++ " --version";
switch (Core_unix.open_process_in(full_cmd) |> In_channel.input_line) {
| Some(stdout) => {
let substrings = Re.exec(semver_re, stdout);
let groups = Re.Group.all(substrings);
Ok({
major: groups[1] |> int_of_string,
minor: groups[2] |> int_of_string,
point: groups[3] |> int_of_string
})
}
| None => Error(Printf.sprintf("Failed executing '%s'", cmd))
}
};
let is_version_new_enough = (cur, needed) => {
switch (cur) {
| Ok(cur) => {
switch (compare_version_number(cur, needed)) {
| x when x < 0 => Ok(false)
| _ => Ok(true)
}
}
| Error(e) => Error(e)
}
}
let preflight_versions = () => {
let in_direnv = switch (Sys.getenv("direnv")) {
| Some(_) => true
| None => false
};
let is_nix_new_enough = is_version_new_enough(extract_version_number("nix"), required_nix_version);
let is_direnv_new_enough = is_version_new_enough(extract_version_number("direnv"), required_direnv_version);
switch (in_direnv, is_direnv_new_enough, is_nix_new_enough) {
| (false, _, _) => Error("Not in direnv!")
| (_, Ok(false), _) => Error("Direnv version is not new enough")
| (_, _, Ok(false)) => Error("Nix version is not new enough")
| (_, Error(e), _) => Error(e)
| (_, _, Error(e)) => Error(e)
| (true, Ok(true), Ok(true)) => Ok()
}
};
let preflight = (layout_directory) => {
switch (preflight_versions()) {
| Ok(_) => {
switch (Sys_unix.is_directory(layout_directory)) {
| `Yes => Ok()
| _ => {
Unix.mkdir_p(layout_directory);
Ok()
}
}
}
| err => err
}
};
let hash_files = (filenames) => {
let ctx = Sha1.init();
let () = filenames
|> Array.filter(~f=f => switch (Sys_unix.file_exists(f)) {
| `Yes => true
| _ => false
})
|> Array.iter(~f=(f) => {
f |> In_channel.create |> In_channel.input_all |> Sha1.update_string(ctx);
});
Sha1.finalize(ctx) |> Sha1.to_hex
};
let get_watches = () => {
let direnv_watch_str = Sys.getenv("DIRENV_WATCHES") |> Option.value_exn(~message="Environment missing DIRENV_WATCHES");
let proc_info = Unix.create_process(~prog="direnv", ~args=["show_dump", direnv_watch_str]);
let sub_stdout = Unix.in_channel_of_descr(proc_info.stdout);
switch (Unix.waitpid(proc_info.pid)) {
| Ok() => Ok(watches_of_yojson(Yojson.Safe.from_channel(sub_stdout)))
| _ => Error("Failed to parse watches")
}
};
let rec rmrf = (path) => switch (Sys_unix.is_directory(~follow_symlinks=false, path)) {
| `Yes => {
Sys_unix.readdir(path) |> Array.iter(~f=name => { print_endline(Filename.concat(path, name)); rmrf(Filename.concat(path, name))});
Unix.rmdir(path)
}
| `No => {
switch (Sys_unix.is_file(~follow_symlinks=false, path)) {
| `Yes => Sys_unix.remove(path)
| _ => ()
}
}
| `Unknown => {
Printf.eprintf("Cannot determine what file type of path %s", path);
exit(1);
}
};
let clean_old_gcroots = (layout_dir) => {
rmrf(layout_dir ++ "/flake-inputs/");
rmrf(layout_dir);
Unix.mkdir_p(layout_dir ++ "/flake-inputs/");
};
let print_cur_cache = (profile_rc) => {
In_channel.read_all(profile_rc) |> Printf.printf("%s")
};
let nix = (args) => {
let stdout_chan = Unix.open_process_in(
"nix --extra-experimental-features \"nix-command flakes\" " ++ (args |> String.concat(~sep=" ")))
let stdout_content = stdout_chan |> In_channel.input_all;
let exit_code = Unix.close_process_in(stdout_chan);
(exit_code, stdout_content)
}
let freshen_cache = (layout_dir, hash, flake_specifier) => {
clean_old_gcroots(layout_dir);
let tmp_profile = layout_dir ++ "flake-tmp-profile." ++ Core.Pid.to_string(Core_unix.getpid());
let (exit_code, stdout_content) = nix(["print-dev-env", "--profile", tmp_profile, flake_specifier]);
let profile = layout_dir ++ "/flake-profile-" ++ hash;
let profile_rc = profile ++ ".rc";
switch (exit_code) {
| Ok() => {
Out_channel.with_file(~f=f=> Out_channel.output_string(f, stdout_content), profile_rc);
switch (nix(["build", "--out-link", profile, tmp_profile])) {
| (Ok(), _) => {
Sys_unix.remove(tmp_profile);
print_cur_cache(profile_rc);
}
| (err, _) => {
Printf.eprintf("Failed creating gcroot: %s\n", Core_unix.Exit_or_signal.to_string_hum(err));
exit(1);
}
};
}
| err => {
Printf.eprintf("Failed evaluating flake: %s\n", Core_unix.Exit_or_signal.to_string_hum(err));
exit(1);
}
};
};
// TODO: Extend to add support for additional flags, maybe?
let main = () => {
let argv = Sys.get_argv();
switch (Array.length(argv)) {
| 3 => {
let flake_specifier = argv[1];
let layout_directory = argv[2];
switch (preflight(layout_directory)) {
| Ok() => {
switch (get_watches()) {
| Ok(watches) => {
let paths = Array.map(~f=watch => watch.path, watches);
let hash = hash_files(paths);
let profile = layout_directory ++ "/flake-profile-" ++ hash;
let profile_rc = profile ++ ".rc";
switch ((Sys_unix.is_file(profile_rc), Sys_unix.is_file(profile))) {
| (`Yes, `Yes) => {
let profile_rc_mtime = Unix.stat(profile_rc).st_mtime;
let all_older= Array.map(~f=watch => watch.modtime, watches)
|> Array.for_all(~f=watch_mtime => watch_mtime <= int_of_float(profile_rc_mtime))
switch (all_older) {
| true => print_cur_cache(profile_rc)
| false => freshen_cache(layout_directory, hash, flake_specifier)
}
}
| _ => freshen_cache(layout_directory, hash, flake_specifier)
}
};
| Error(e) => {
Printf.eprintf("%s", e);
exit(1);
}
}
}
| Error(e) => {
Printf.eprintf("%s", e);
exit(1);
}
};
}
| _ => {
Printf.eprintf("%s <flake specifier> <layout_directory>\n", argv[0]);
exit(1);
}
}
}
let () = main();