diff --git a/specs/phoenix/runner b/specs/phoenix/runner new file mode 100755 index 0000000..9689d79 --- /dev/null +++ b/specs/phoenix/runner @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Exit immediately if the script encounters a non-zero status. +set -e + +# If set, Bash includes filenames beginning with a `.` in the results of +# filename expansion. The filenames `.` and `..` must always be matched +# explicitly, even if dotglob is set. +shopt -s dotglob + +# ============================================================ +# PROLOGUE +# ============================================================ + +# Create a new top-level directory as fallback in case $BUILD (defined below) +# is ever empty. +mkdir -p "/tmp/bs.phoenix" + +# Create an intermediate build directory. The final step of this script will +# copy the content from this directory to $OUT. +BUILD=$(mktemp -d -p "/tmp/bs.phoenix") + +if [ -z "$BUILD" ]; then + >&2 echo "Failed to create temp directory." + exit 1 +fi + +# Deletes the intermediate build directory on exit. We use a concatenation of +# the intermediate directory with the basename of the generated temp directory +# to ensure we never evaluate to root (i.e. `/`). That should never actually +# happen but a good habit to establish nonetheless. +function cleanup { + rm -r "/tmp/bs.phoenix/$(basename "$BUILD")" +} + +trap cleanup EXIT + +# ============================================================ +# BUILD +# ============================================================ + +# Copy template contents over to the intermediate build directory. +cp -r template/* "$BUILD" + +# Explicitly set permissions on all copied files. +find "$BUILD" -type f -execdir chmod 644 {} + +find "$BUILD" -type d -execdir chmod 755 {} + + +# Generate a new project with the specified name using `mix`. Generate in a +# subdirectory to avoid interactive conflict resolution. +nix develop "$BUILD" --command bash -c "mix phx.new $BUILD/project --app '$APP'" + +# Copy the generated files into the intermediate build directory. +mv -f "$BUILD"/project/* "$BUILD" +rmdir "$BUILD"/project + +# Overwrite the generated README in favor of that defined in our template. +cp template/README.md "$BUILD" +chmod 644 "$BUILD"/README.md + +# Replace the template name found in the flake.nix file. The mix generator would +# fail if $APP did not consist of just lowercase ASCII letters, numbers, or +# underscores. Thus $APP is safe to interpolate into the following command. +sed -i "s//$APP/g" "$BUILD/flake.nix" + +# By default Phoenix generates a postgres configuration with assumed username +# `postgres`. This flake encourages a local postgres database with username +# `$(whoami)`. Reflect this in the config. +sed -i "s/username: \"postgres\"/username: \"$(whoami)\"/g" "$BUILD/config/dev.exs" + +# ============================================================ +# EPILOGUE +# ============================================================ + +# Success! Copy contents to target directory. +cp -r "$BUILD"/* "$OUT" diff --git a/specs/phoenix/spec.json b/specs/phoenix/spec.json new file mode 100644 index 0000000..6c191de --- /dev/null +++ b/specs/phoenix/spec.json @@ -0,0 +1,6 @@ +{ + "app": { + "type": "line", + "prompt": "App Name> " + } +} diff --git a/specs/phoenix/template/.envrc b/specs/phoenix/template/.envrc new file mode 100644 index 0000000..b9238c3 --- /dev/null +++ b/specs/phoenix/template/.envrc @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +use flake diff --git a/specs/phoenix/template/README.md b/specs/phoenix/template/README.md new file mode 100644 index 0000000..6ed16bc --- /dev/null +++ b/specs/phoenix/template/README.md @@ -0,0 +1,53 @@ +# Phoenix Flake Template + +This is a template for constructing a environment for Elixir development +(version 1.15.7, Erlang/OTP 25) with the [Phoenix](https://www.phoenixframework.org/) +(version 1.7.10) framework. [direnv](https://direnv.net/) can be used to launch +a dev shell upon entering this directory (refer to `.envrc`). Otherwise run via: +```bash +$> nix develop +``` + +## Quickstart + +To begin, create a new database: +```bash +$> pg_ctl initdb +``` +If the flake's default `devShell` is loaded, this will create a database cluster +at `$PWD/data`. To start the database, run the following: +```bash +$> pg_ctl start -o --unix_socket_directories="$PGDATA" +``` +To shut the database down, run: +```bash +$> pg_ctl stop +``` +You can also specify a different location for the database cluster using the +`-D` option in each of the above commands. + +Afterward, you can run the Phoenix setup commands: +```bash +$> mix ecto.setup +$> mix assets.setup +``` +and then start the local server: +```bash +$> mix phx.server +``` + +## Language Server + +The [elixir-ls](https://github.com/elixir-lsp/elixir-ls) LSP (version 0.17.10) +is included in this flake. + +## Dependencies + +This project pins Mix dependencies using [mix2nix](https://github.com/ydlr/mix2nix). +After updating your `mix.lock` file, make sure to re-run the following: +```bash +mix2nix > deps.nix +``` +As of now, `mix2nix` cannot handle git dependencies found inside the `mix.lock` +file. If you have git dependencies, add them manually or use +[FODs](https://nixos.org/manual/nixpkgs/stable/#packaging-beam-applications). diff --git a/specs/phoenix/template/default.nix b/specs/phoenix/template/default.nix new file mode 100644 index 0000000..f620865 --- /dev/null +++ b/specs/phoenix/template/default.nix @@ -0,0 +1,10 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).defaultNix diff --git a/specs/phoenix/template/deps.nix b/specs/phoenix/template/deps.nix new file mode 100644 index 0000000..3fde5b3 --- /dev/null +++ b/specs/phoenix/template/deps.nix @@ -0,0 +1,545 @@ +{ lib, beamPackages, overrides ? (x: y: {}) }: + +let + buildRebar3 = lib.makeOverridable beamPackages.buildRebar3; + buildMix = lib.makeOverridable beamPackages.buildMix; + buildErlangMk = lib.makeOverridable beamPackages.buildErlangMk; + + self = packages // (overrides self packages); + + packages = with beamPackages; with self; { + castore = buildMix rec { + name = "castore"; + version = "1.0.4"; + + src = fetchHex { + pkg = "castore"; + version = "${version}"; + sha256 = "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"; + }; + + beamDeps = []; + }; + + cowboy = buildErlangMk rec { + name = "cowboy"; + version = "2.10.0"; + + src = fetchHex { + pkg = "cowboy"; + version = "${version}"; + sha256 = "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"; + }; + + beamDeps = [ cowlib ranch ]; + }; + + cowboy_telemetry = buildRebar3 rec { + name = "cowboy_telemetry"; + version = "0.4.0"; + + src = fetchHex { + pkg = "cowboy_telemetry"; + version = "${version}"; + sha256 = "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"; + }; + + beamDeps = [ cowboy telemetry ]; + }; + + cowlib = buildRebar3 rec { + name = "cowlib"; + version = "2.12.1"; + + src = fetchHex { + pkg = "cowlib"; + version = "${version}"; + sha256 = "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"; + }; + + beamDeps = []; + }; + + db_connection = buildMix rec { + name = "db_connection"; + version = "2.6.0"; + + src = fetchHex { + pkg = "db_connection"; + version = "${version}"; + sha256 = "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"; + }; + + beamDeps = [ telemetry ]; + }; + + decimal = buildMix rec { + name = "decimal"; + version = "2.1.1"; + + src = fetchHex { + pkg = "decimal"; + version = "${version}"; + sha256 = "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"; + }; + + beamDeps = []; + }; + + dns_cluster = buildMix rec { + name = "dns_cluster"; + version = "0.1.1"; + + src = fetchHex { + pkg = "dns_cluster"; + version = "${version}"; + sha256 = "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"; + }; + + beamDeps = []; + }; + + ecto = buildMix rec { + name = "ecto"; + version = "3.11.0"; + + src = fetchHex { + pkg = "ecto"; + version = "${version}"; + sha256 = "7769dad267ef967310d6e988e92d772659b11b09a0c015f101ce0fff81ce1f81"; + }; + + beamDeps = [ decimal jason telemetry ]; + }; + + ecto_sql = buildMix rec { + name = "ecto_sql"; + version = "3.11.0"; + + src = fetchHex { + pkg = "ecto_sql"; + version = "${version}"; + sha256 = "77aa3677169f55c2714dda7352d563002d180eb33c0dc29cd36d39c0a1a971f5"; + }; + + beamDeps = [ db_connection ecto postgrex telemetry ]; + }; + + esbuild = buildMix rec { + name = "esbuild"; + version = "0.8.1"; + + src = fetchHex { + pkg = "esbuild"; + version = "${version}"; + sha256 = "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"; + }; + + beamDeps = [ castore jason ]; + }; + + expo = buildMix rec { + name = "expo"; + version = "0.4.1"; + + src = fetchHex { + pkg = "expo"; + version = "${version}"; + sha256 = "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"; + }; + + beamDeps = []; + }; + + file_system = buildMix rec { + name = "file_system"; + version = "0.2.10"; + + src = fetchHex { + pkg = "file_system"; + version = "${version}"; + sha256 = "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"; + }; + + beamDeps = []; + }; + + finch = buildMix rec { + name = "finch"; + version = "0.16.0"; + + src = fetchHex { + pkg = "finch"; + version = "${version}"; + sha256 = "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"; + }; + + beamDeps = [ castore mime mint nimble_options nimble_pool telemetry ]; + }; + + floki = buildMix rec { + name = "floki"; + version = "0.35.2"; + + src = fetchHex { + pkg = "floki"; + version = "${version}"; + sha256 = "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"; + }; + + beamDeps = []; + }; + + gettext = buildMix rec { + name = "gettext"; + version = "0.23.1"; + + src = fetchHex { + pkg = "gettext"; + version = "${version}"; + sha256 = "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"; + }; + + beamDeps = [ expo ]; + }; + + hpax = buildMix rec { + name = "hpax"; + version = "0.1.2"; + + src = fetchHex { + pkg = "hpax"; + version = "${version}"; + sha256 = "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"; + }; + + beamDeps = []; + }; + + jason = buildMix rec { + name = "jason"; + version = "1.4.1"; + + src = fetchHex { + pkg = "jason"; + version = "${version}"; + sha256 = "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"; + }; + + beamDeps = [ decimal ]; + }; + + mime = buildMix rec { + name = "mime"; + version = "2.0.5"; + + src = fetchHex { + pkg = "mime"; + version = "${version}"; + sha256 = "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"; + }; + + beamDeps = []; + }; + + mint = buildMix rec { + name = "mint"; + version = "1.5.1"; + + src = fetchHex { + pkg = "mint"; + version = "${version}"; + sha256 = "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"; + }; + + beamDeps = [ castore hpax ]; + }; + + nimble_options = buildMix rec { + name = "nimble_options"; + version = "1.0.2"; + + src = fetchHex { + pkg = "nimble_options"; + version = "${version}"; + sha256 = "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"; + }; + + beamDeps = []; + }; + + nimble_pool = buildMix rec { + name = "nimble_pool"; + version = "1.0.0"; + + src = fetchHex { + pkg = "nimble_pool"; + version = "${version}"; + sha256 = "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"; + }; + + beamDeps = []; + }; + + phoenix = buildMix rec { + name = "phoenix"; + version = "1.7.10"; + + src = fetchHex { + pkg = "phoenix"; + version = "${version}"; + sha256 = "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"; + }; + + beamDeps = [ castore jason phoenix_pubsub phoenix_template plug plug_cowboy plug_crypto telemetry websock_adapter ]; + }; + + phoenix_ecto = buildMix rec { + name = "phoenix_ecto"; + version = "4.4.3"; + + src = fetchHex { + pkg = "phoenix_ecto"; + version = "${version}"; + sha256 = "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"; + }; + + beamDeps = [ ecto phoenix_html plug ]; + }; + + phoenix_html = buildMix rec { + name = "phoenix_html"; + version = "3.3.3"; + + src = fetchHex { + pkg = "phoenix_html"; + version = "${version}"; + sha256 = "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"; + }; + + beamDeps = [ plug ]; + }; + + phoenix_live_dashboard = buildMix rec { + name = "phoenix_live_dashboard"; + version = "0.8.3"; + + src = fetchHex { + pkg = "phoenix_live_dashboard"; + version = "${version}"; + sha256 = "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"; + }; + + beamDeps = [ ecto mime phoenix_live_view telemetry_metrics ]; + }; + + phoenix_live_reload = buildMix rec { + name = "phoenix_live_reload"; + version = "1.4.1"; + + src = fetchHex { + pkg = "phoenix_live_reload"; + version = "${version}"; + sha256 = "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"; + }; + + beamDeps = [ file_system phoenix ]; + }; + + phoenix_live_view = buildMix rec { + name = "phoenix_live_view"; + version = "0.20.1"; + + src = fetchHex { + pkg = "phoenix_live_view"; + version = "${version}"; + sha256 = "be494fd1215052729298b0e97d5c2ce8e719c00854b82cd8cf15c1cd7fcf6294"; + }; + + beamDeps = [ jason phoenix phoenix_html phoenix_template plug telemetry ]; + }; + + phoenix_pubsub = buildMix rec { + name = "phoenix_pubsub"; + version = "2.1.3"; + + src = fetchHex { + pkg = "phoenix_pubsub"; + version = "${version}"; + sha256 = "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"; + }; + + beamDeps = []; + }; + + phoenix_template = buildMix rec { + name = "phoenix_template"; + version = "1.0.3"; + + src = fetchHex { + pkg = "phoenix_template"; + version = "${version}"; + sha256 = "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"; + }; + + beamDeps = [ phoenix_html ]; + }; + + plug = buildMix rec { + name = "plug"; + version = "1.15.2"; + + src = fetchHex { + pkg = "plug"; + version = "${version}"; + sha256 = "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"; + }; + + beamDeps = [ mime plug_crypto telemetry ]; + }; + + plug_cowboy = buildMix rec { + name = "plug_cowboy"; + version = "2.6.1"; + + src = fetchHex { + pkg = "plug_cowboy"; + version = "${version}"; + sha256 = "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"; + }; + + beamDeps = [ cowboy cowboy_telemetry plug ]; + }; + + plug_crypto = buildMix rec { + name = "plug_crypto"; + version = "2.0.0"; + + src = fetchHex { + pkg = "plug_crypto"; + version = "${version}"; + sha256 = "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"; + }; + + beamDeps = []; + }; + + postgrex = buildMix rec { + name = "postgrex"; + version = "0.17.3"; + + src = fetchHex { + pkg = "postgrex"; + version = "${version}"; + sha256 = "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"; + }; + + beamDeps = [ db_connection decimal jason ]; + }; + + ranch = buildRebar3 rec { + name = "ranch"; + version = "1.8.0"; + + src = fetchHex { + pkg = "ranch"; + version = "${version}"; + sha256 = "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"; + }; + + beamDeps = []; + }; + + swoosh = buildMix rec { + name = "swoosh"; + version = "1.14.2"; + + src = fetchHex { + pkg = "swoosh"; + version = "${version}"; + sha256 = "01d8fae72930a0b5c1bb9725df0408602ed8c5c3d59dc6e7a39c57b723cd1065"; + }; + + beamDeps = [ cowboy finch jason mime plug plug_cowboy telemetry ]; + }; + + tailwind = buildMix rec { + name = "tailwind"; + version = "0.2.2"; + + src = fetchHex { + pkg = "tailwind"; + version = "${version}"; + sha256 = "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"; + }; + + beamDeps = [ castore ]; + }; + + telemetry = buildRebar3 rec { + name = "telemetry"; + version = "1.2.1"; + + src = fetchHex { + pkg = "telemetry"; + version = "${version}"; + sha256 = "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"; + }; + + beamDeps = []; + }; + + telemetry_metrics = buildMix rec { + name = "telemetry_metrics"; + version = "0.6.1"; + + src = fetchHex { + pkg = "telemetry_metrics"; + version = "${version}"; + sha256 = "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"; + }; + + beamDeps = [ telemetry ]; + }; + + telemetry_poller = buildRebar3 rec { + name = "telemetry_poller"; + version = "1.0.0"; + + src = fetchHex { + pkg = "telemetry_poller"; + version = "${version}"; + sha256 = "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"; + }; + + beamDeps = [ telemetry ]; + }; + + websock = buildMix rec { + name = "websock"; + version = "0.5.3"; + + src = fetchHex { + pkg = "websock"; + version = "${version}"; + sha256 = "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"; + }; + + beamDeps = []; + }; + + websock_adapter = buildMix rec { + name = "websock_adapter"; + version = "0.5.5"; + + src = fetchHex { + pkg = "websock_adapter"; + version = "${version}"; + sha256 = "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"; + }; + + beamDeps = [ plug plug_cowboy websock ]; + }; + }; +in self + diff --git a/specs/phoenix/template/flake.lock b/specs/phoenix/template/flake.lock new file mode 100644 index 0000000..035b7da --- /dev/null +++ b/specs/phoenix/template/flake.lock @@ -0,0 +1,76 @@ +{ + "nodes": { + "flake-compat": { + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "revCount": 57, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1700794826, + "narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/specs/phoenix/template/flake.nix b/specs/phoenix/template/flake.nix new file mode 100644 index 0000000..6a610e3 --- /dev/null +++ b/specs/phoenix/template/flake.nix @@ -0,0 +1,63 @@ +{ + description = '' + An opinionated pheonix flake. + + To generate a copy of this template elsewhere, run: + $> bootstrap phoenix + ''; + + inputs = { + flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + inherit (pkgs.beam.packages.erlang_25) + beamPackages + elixir + elixir-ls + hex + mixRelease; + + deps = import ./deps.nix { + lib = pkgs.lib; + inherit beamPackages; + }; + in + { + packages = { + app = mixRelease { + pname = ""; + src = ./.; + version = "0.1.0"; + mixNixDeps = deps; + }; + + default = self.packages.${system}.app; + }; + + devShells.default = pkgs.mkShell { + packages = [ + elixir + elixir-ls + hex + ] ++ (with pkgs; [ + inotify-tools # For file watching in development. + mix2nix + postgresql_15 + ]); + shellHook = '' + # The server will try to use the data directory named by this + # environment variable whenever `-D` is not specified (for e.g. + # `postgres` and `pg_ctl`). + # https://www.postgresql.org/docs/15/server-start.html + export PGDATA="$PWD/data" + ''; + }; + }); +}