diff --git a/.github/workflows/check-static.yaml b/.github/workflows/check-static.yaml new file mode 100644 index 0000000..3570368 --- /dev/null +++ b/.github/workflows/check-static.yaml @@ -0,0 +1,16 @@ +name: "Check Static Build" + +on: + workflow_dispatch: + +jobs: + check: + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout Codebase" + uses: "actions/checkout@v6" + + - name: "Build Static Exectutable" + run: | + bash ./build-static.sh diff --git a/.github/workflows/check.yaml b/.github/workflows/check-verify.yaml similarity index 51% rename from .github/workflows/check.yaml rename to .github/workflows/check-verify.yaml index 29926f6..c20daf1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check-verify.yaml @@ -1,4 +1,4 @@ -name: "Check Codebase" +name: "Check, Lint, Test and Build Codebase" on: pull_request: @@ -10,14 +10,18 @@ jobs: steps: - name: "Checkout Codebase" - uses: "actions/checkout@v4" + uses: "actions/checkout@v6" - name: "Install Nix" - uses: "DeterminateSystems/nix-installer-action@v19" + uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Check, Test and Build" + - name: "Prepare CI devShell" run: | - nix develop --command bash -c "cabal update --ignore-project && cabal dev-test-build" + nix develop .#ci --command true + + - name: "Verify Codebase" + run: | + nix develop .#ci --command cabal verify - name: "Build Docker Image" run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e0a806..a4f5605 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,22 +20,24 @@ jobs: - name: "Checkout Codebase" if: "${{ steps.release.outputs.release_created }}" - uses: "actions/checkout@v4" + uses: "actions/checkout@v6" with: fetch-depth: 0 - name: "Install Nix" if: "${{ steps.release.outputs.release_created }}" - uses: "DeterminateSystems/nix-installer-action@v19" + uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Build Application" + - name: "Build Static Exectutable" + id: "build_static" if: "${{ steps.release.outputs.release_created }}" run: | - nix develop --command bash -c "bash build-static.sh" + bash ./build-static.sh | tee /tmp/build.log + echo "executable=$(tail -n1 /tmp/build.log)" >> $GITHUB_OUTPUT - name: "Upload Release Artifact" if: "${{ steps.release.outputs.release_created }}" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | - gh release upload "${{ steps.release.outputs.tag_name }}" /tmp/clompse-static-linux-x86_64 + gh release upload "${{ steps.release.outputs.tag_name }}" "${{ steps.build_static.outputs.executable }}" diff --git a/.gitignore b/.gitignore index d5a3361..d5deba7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ *.cabal *~ /.direnv +/.envrc +/.stack-work /config.yaml /dist /dist-newstyle /result +/stack.yaml.lock /tmp diff --git a/.prettierignore b/.prettierignore index a8a21b4..2aa6043 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ +CHANGELOG.md +LICENSE.md dist-newstyle/ dist/ -nix/ -*.md diff --git a/.prettierrc.json b/.prettierrc.json index aae5213..51edb1d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,9 +1,16 @@ { "tabWidth": 2, + "printWidth": 120, "singleQuote": false, "trailingComma": "es5", - "printWidth": 120, "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 80, + "proseWrap": "always" + } + }, { "files": "package.yaml", "options": { diff --git a/.stan.toml b/.stan.toml new file mode 100644 index 0000000..54246a4 --- /dev/null +++ b/.stan.toml @@ -0,0 +1,8 @@ +# Big tuples +# Using tuples of big size (>= 4) can decrease code readability +# In serveral places Stack uses 4-tuples and in one place Stack uses a +# 5-tuple. +[[check]] +id = "STAN-0302" +scope = "all" +type = "Exclude" diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..93da9dd --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,25 @@ +#:schema taplo://taplo.toml + +include = ["*.toml"] +exclude = [] + +[formatting] + +align_entries = false # Align entries vertically. Entries that have table headers, comments, or blank lines between them are not aligned.(default false) +align_comments = true # Align consecutive comments after entries and items vertically. This applies to comments that are after entries or array items.(default true) +array_trailing_comma = true # Put trailing commas for multiline arrays.(default true) +array_auto_expand = true # Automatically expand arrays to multiple lines (default true) +array_auto_collapse = false # Automatically collapse arrays if they fit in one line.(default true) +compact_arrays = true # Omit whitespace padding inside single-line arrays.(default true) +compact_inline_tables = false # Omit whitespace padding inside inline tables.(default false) +inline_table_expand = true # Expand values (e.g. arrays) inside inline tables.(default true) +compact_entries = false # Omit whitespace around =. (default false) +column_width = 80 # Target maximum column width after which arrays are expanded into new lines.(default 80) +indent_tables = false # Indent subtables if they come in order(default false) +indent_entries = false # Indent entries under tables.(default false) +indent_string = " " # Indentation to use, should be tabs or spaces but technically could be anything. 2 spaces (" ") +trailing_newline = true # Add trailing newline to the source. (default true) +reorder_keys = false # Alphabetically reorder keys that are not separated by blank lines. (default false) +reorder_arrays = false # Alphabetically reorder array values that are not separated by blank lines. (default false) +allowed_blank_lines = 1 # The maximum amount of consecutive blank lines allowed. (default 2) +crlf = false # Use CRLF line endings. (default false) diff --git a/README.md b/README.md index c576d20..d85ccd8 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,24 @@ > [!WARNING] > -> This is an experimental project that is in its early stages of -> development. Both the functionality and API are subject to change at -> any time. +> This is an experimental project that is in its early stages of development. +> Both the functionality and API are subject to change at any time. > -> It is not recommended to use this in production or any other -> critical environment. Use at your own risk. +> It is not recommended to use this in production or any other critical +> environment. Use at your own risk. -**clompse** is a command-line tool designed to provide a unified -interface to various cloud providers, helping you manage a registry of -your cloud resources. +**clompse** is a command-line tool designed to provide a unified interface to +various cloud providers, helping you manage a registry of your cloud resources. -It consumes a JSON/YAML formatted configuration file that lists -various profiles where each contains a number of cloud provider API -credentials. It then allows you to query and list cloud resources -across these providers. The output can be displayed in tabular format -on the console or exported to a file in CSV or JSON format. +It consumes a JSON/YAML formatted configuration file that lists various profiles +where each contains a number of cloud provider API credentials. It then allows +you to query and list cloud resources across these providers. The output can be +displayed in tabular format on the console or exported to a file in CSV or JSON +format. -Currently, it supports listing cloud servers with their firewall -configurations, object storage buckets, DNS zones, and records for the -following cloud service providers: +Currently, it supports listing cloud servers with their firewall configurations, +object storage buckets, DNS zones, and records for the following cloud service +providers: 1. Amazon Web Services 2. DigitalOcean @@ -40,52 +38,48 @@ following cloud service providers: ## Motivation -Using cloud services has become common for many individuals and -organizations. As the number of cloud resources grows, it becomes -increasingly difficult to keep track of them. This tool aims to -provide a unified interface to query and list commonly-used cloud -resources across different cloud providers. +Using cloud services has become common for many individuals and organizations. +As the number of cloud resources grows, it becomes increasingly difficult to +keep track of them. This tool aims to provide a unified interface to query and +list commonly-used cloud resources across different cloud providers. -While there are many tools and SaaS solutions that offer similar or -more advanced functionality, they are often too complex, too -complicated, or too expensive for individual hackers. This tool is a -more humble alternative to such solutions. +While there are many tools and SaaS solutions that offer similar or more +advanced functionality, they are often too complex, too complicated, or too +expensive for individual hackers. This tool is a more humble alternative to such +solutions. ## Non-Motivation -This tool is not intended to be a full-fledged cloud management -tool. It is not a replacement for the cloud providers' own management -consoles or APIs. +This tool is not intended to be a full-fledged cloud management tool. It is not +a replacement for the cloud providers' own management consoles or APIs. -It also does not provide functionality to create, update, or delete -cloud resources, nor is such functionality planned: it is a read-only -interface to cloud resources. +It also does not provide functionality to create, update, or delete cloud +resources, nor is such functionality planned: it is a read-only interface to +cloud resources. ## Challenge -Not all cloud providers offer APIs that list resources as conveniently -as others. Additionally, the exposed properties of similar resources -may differ between providers. +Not all cloud providers offer APIs that list resources as conveniently as +others. Additionally, the exposed properties of similar resources may differ +between providers. -To address this, the tool defines common data types for cloud -resources and maps cloud provider-specific data types to these common -types. It is not always possible to map all properties of a cloud -resource to a common data type. In such cases, the tool will assume -some (hopefully) reasonable defaults or omit such properties -altogether. +To address this, the tool defines common data types for cloud resources and maps +cloud provider-specific data types to these common types. It is not always +possible to map all properties of a cloud resource to a common data type. In +such cases, the tool will assume some (hopefully) reasonable defaults or omit +such properties altogether. ## Installation -Download and install the statically compiled binary for Linux -(x86_64): +Download and install the statically compiled binary for Linux (x86_64): ```sh curl -Lo /tmp/clompse https://github.com/vst/clompse/releases/latest/download/clompse-static-linux-x86_64 sudo install -m 755 /tmp/clompse /usr/local/bin/clompse ``` -Or install it into your `nix` profile (replace `` with the -latest version): +Or install it into your `nix` profile (replace `` with the latest +version): ```sh nix profile install --file https://github.com/vst/clompse/archive/v.tar.gz @@ -163,25 +157,16 @@ Provision Nix shell via `direnv`: direnv allow ``` -Big, long build command for the impatient: +Check, lint, test and build everything with this: ```sh -hpack && - direnv reload && - fourmolu -i app/ src/ test/ && - prettier --write . && - find . -iname "*.nix" -not -path "*/nix/sources.nix" -print0 | xargs --null nixpkgs-fmt && - hlint app/ src/ test/ && - cabal build -O0 && - cabal run -O0 clompse -- --version && - cabal v1-test && - cabal haddock -O0 +cabal verify [-c | --clean] ``` ## License -Copyright © 2024-2025 Vehbi Sinan Tunalioglu. This work is licensed -under [MIT License]. +Copyright © 2024-2025 Vehbi Sinan Tunalioglu. This work is licensed under +[MIT License]. diff --git a/build-static.sh b/build-static.sh index 151b5db..f6374f1 100644 --- a/build-static.sh +++ b/build-static.sh @@ -1,25 +1,21 @@ #!/usr/bin/env bash -## Fail on errors including pipe failures: -set -eo pipefail - ## NOTE: Things would be much easier if we could use Nix, but we can -## not (or I find it rather tedious). So, we have to use Docker. -## -## Also, `cabal install` does not work with -## `--enable-executable-static` flag. So, we have to use `cabal build` -## instead. Finally, `cabal build` does not work with -## `--enable-executable-stripping`, hence the `strip` command usage. +## not (or I find it rather tedious). So, we have to use Docker and +## Haskell Stack to build our static binary. + +## Executable name: +EXECUTABLE_NAME="$(yq ".executables | keys | .[0]" package.yaml)" + +## Stackage resolver: +STACKAGE_RESOLVER="$(yq ".resolver" stack.yaml)" ## GHC version: -GHC_VERSION="9.8.4" +GHC_VERSION="$(curl -s "https://www.stackage.org/${STACKAGE_RESOLVER}" | grep -oP 'ghc-\K[0-9.]+' | head -n1)" ## Docker image: DOCKER_IMAGE="quay.io/benz0li/ghc-musl:${GHC_VERSION}" -## Executable name: -EXECUTABLE_NAME="clompse" - ## Final executable name: FINAL_EXECUTABLE_NAME="${EXECUTABLE_NAME}-static-$(uname --kernel-name | tr '[:upper:]' '[:lower:]')-$(uname --machine)" @@ -29,61 +25,34 @@ FINAL_EXECUTABLE_PATH="/tmp/${FINAL_EXECUTABLE_NAME}" ## Docker container name: CONTAINER_NAME="static-builder-for-${EXECUTABLE_NAME}" -## Create/update .cabal file: -hpack - -## Update package list: -cabal update - -## Cleanup first: -cabal clean -cabal v1-clean - -## First, pin all packages as per Nix: -cabal freeze - -## Fix tls package version (Remove the line that contains "any.tls ==2.1.1,"): -## -## Note that this is a workaround until nixpkgs provides tls>=2.1.1 as stock dependency. -sed -i '/any.tls ==2.1.1,/d' cabal.project.freeze - ## Run the Docker container: docker run -i --detach -v "$(pwd):/app" --name "${CONTAINER_NAME}" "${DOCKER_IMAGE}" /bin/bash ## Whitelist codebase directory for Git queries: docker exec "${CONTAINER_NAME}" git config --global --add safe.directory /app -## Update cabal database: -docker exec "${CONTAINER_NAME}" cabal update +## Cleanup inside the container: +docker exec -w "/app" "${CONTAINER_NAME}" cabal clean +docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full ## Build the static binary: -## -## Note that the `--allow-newer` flag is used to allow newer versions of -## dependencies, which is to allow jail-broken nixpkgs dependencies in a -## non-nix build environment that adopts our .freeze file generated under -## Nix. -docker exec -w "/app" "${CONTAINER_NAME}" cabal build --enable-executable-static --allow-newer - -## Get the path to the executable: -## -## Note that the `--allow-newer` flag is used to allow newer versions of -## dependencies, which is to allow jail-broken nixpkgs dependencies in a -## non-nix build environment that adopts our .freeze file generated under -## Nix. -BUILD_PATH="$(docker exec -w "/app" "${CONTAINER_NAME}" cabal list-bin --allow-newer "${EXECUTABLE_NAME}")" - -## Strip debugging symbols: -docker exec "${CONTAINER_NAME}" strip "${BUILD_PATH}" +docker exec -w "/app" "${CONTAINER_NAME}" stack build -## Copy the binary to the host: -docker cp "${CONTAINER_NAME}:${BUILD_PATH}" "${FINAL_EXECUTABLE_PATH}" +## Install the static binary to our local-bin-path (/tmp): +docker exec -w "/app" "${CONTAINER_NAME}" stack install + +## Install upx: +docker exec -w "/app" "${CONTAINER_NAME}" apk add upx ## Compress the executable: -upx "${FINAL_EXECUTABLE_PATH}" +docker exec -w "/app" "${CONTAINER_NAME}" upx "/tmp/${EXECUTABLE_NAME}" + +## Copy the binary to the host: +docker cp "${CONTAINER_NAME}:/tmp/${EXECUTABLE_NAME}" "${FINAL_EXECUTABLE_PATH}" ## Cleanup: -docker exec -w "/app" "${CONTAINER_NAME}" cabal clean -docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full docker rm -f "${CONTAINER_NAME}" -rm cabal.project.freeze file "${FINAL_EXECUTABLE_PATH}" +find "${FINAL_EXECUTABLE_PATH}" diff --git a/cabal.project b/cabal.project index 4b620a2..fdadb42 100644 --- a/cabal.project +++ b/cabal.project @@ -3,32 +3,3 @@ packages: package * ghc-options: -fwrite-ide-info - --- BEGIN WORKAROUND --- --- This workaround is needed to avoid duplicate record field errors on amazonka --- libraries as per migration to GHC 9.8.4. --- --- Once all amazonka libraries are updated for GHC 9.8 and/or later, we can --- remove this workaround. -package amazonka-ec2 - ghc-options: -XDuplicateRecordFields - -package amazonka-lightsail - ghc-options: -XDuplicateRecordFields - -package amazonka-route53 - ghc-options: -XDuplicateRecordFields - -package amazonka-s3 - ghc-options: -XDuplicateRecordFields - -package amazonka-sso - ghc-options: -XDuplicateRecordFields - -package amazonka-sts - ghc-options: -XDuplicateRecordFields - -package amazonka - ghc-options: -XDuplicateRecordFields --- END WORKAROUND diff --git a/flake.lock b/flake.lock index 007299b..b1a714b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,59 +1,59 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1764316264, - "narHash": "sha256-82L+EJU+40+FIdeG4gmUlOF1jeSwlf2AwMarrpdHF6o=", - "owner": "nixos", + "lastModified": 1766473571, + "narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "9a7b80b6f82a71ea04270d7ba11b48855681c4b0", + "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-25.05", + "owner": "NixOS", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { + "nixpkgs-lib": { "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "nix-community", + "repo": "nixpkgs.lib", "type": "github" } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 3620878..33fd2a3 100644 --- a/flake.nix +++ b/flake.nix @@ -2,132 +2,154 @@ description = "clompse - Patrol Cloud Resources"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-parts.url = "github:hercules-ci/flake-parts"; }; - outputs = { self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - ## Import nixpkgs: - pkgs = import nixpkgs { inherit system; }; - - ## Load readYAML helper: - readYAML = pkgs.callPackage ./nix/read-yaml.nix { }; - - ## Read package information: - package = readYAML ./package.yaml; - - ## Get our Haskell: - thisHaskell = pkgs.haskellPackages.override { - overrides = self: super: { - ${package.name} = self.callCabal2nix package.name ./. { }; + outputs = + inputs@{ nixpkgs, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + ./nix/flake-modules/read-yaml + ]; + + systems = nixpkgs.lib.systems.flakeExposed; + + perSystem = + { + config, + self', + inputs', + pkgs, + system, + readYAML, + ... + }: + let + ## Read package information: + package = readYAML ./package.yaml; + + ## Get our Haskell: + thisHaskell = pkgs.haskellPackages.override { + overrides = self: super: { + ${package.name} = self.callCabal2nix package.name ./. { }; + }; }; - }; - - ## Prepare dev-test-build script: - dev-test-build = pkgs.writeShellApplication { - name = "cabal-dev-test-build"; - text = builtins.readFile ./nix/dev-test-build.sh; - runtimeInputs = [ pkgs.bash pkgs.bc pkgs.moreutils ]; - }; - - ## Prepare Nix shell: - thisShell = thisHaskell.shellFor { - ## Define packages for the shell: - packages = p: [ p.${package.name} ]; - - ## Enable Hoogle: - withHoogle = false; - ## Build inputs for development shell: - buildInputs = [ - ## Haskell related build inputs: - thisHaskell.apply-refact - thisHaskell.cabal-fmt + ## Common build inputs for both development and CI environments: + buildInputsCommon = [ + ## Essential Haskell tools: thisHaskell.cabal-install - thisHaskell.cabal2nix thisHaskell.fourmolu - thisHaskell.haskell-language-server thisHaskell.hlint thisHaskell.hpack + thisHaskell.stan thisHaskell.weeder + ## Other essentials: + pkgs.git + pkgs.nixfmt-rfc-style + pkgs.prettier + pkgs.shellcheck + pkgs.shfmt + pkgs.statix + pkgs.taplo + ## Our development scripts: - dev-test-build + (pkgs.callPackage ./nix/cabal-verify { }) + ]; - ## Other build inputs for various development requirements: + ## Development-only inputs: + buildInputsDevOnly = [ + ## Haskell development tools: + thisHaskell.haskell-language-server + thisHaskell.cabal-fmt + thisHaskell.cabal2nix + + ## Other development tools: pkgs.docker-client - pkgs.git pkgs.nil - pkgs.nixpkgs-fmt - pkgs.nodePackages.prettier - pkgs.upx ]; - }; - thisPackage = pkgs.haskell.lib.justStaticExecutables ( - thisHaskell.${package.name}.overrideAttrs (oldAttrs: { - nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ - pkgs.git - pkgs.installShellFiles - pkgs.makeWrapper - pkgs.ronn - ]; - - postFixup = (oldAttrs.postFixup or "") + '' - ## Create output directories: - mkdir -p $out/{bin} - - ## Wrap program to add PATHs to dependencies: - wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath [ - pkgs.bashInteractive ## Added for bash-based CLI option completions - ]} - - ## Install completion scripts: - installShellCompletion --bash --name ${package.name}.bash <($out/bin/${package.name} --bash-completion-script "$out/bin/${package.name}") - installShellCompletion --fish --name ${package.name}.fish <($out/bin/${package.name} --fish-completion-script "$out/bin/${package.name}") - installShellCompletion --zsh --name _${package.name} <($out/bin/${package.name} --zsh-completion-script "$out/bin/${package.name}") - ''; - }) - ); - - thisDocker = pkgs.dockerTools.buildImage { - name = "${package.name}"; - tag = "v${package.version}"; - created = "now"; - - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = [ pkgs.cacert ]; - pathsToLink = [ "/etc" ]; + ## Development shell: + devShell = thisHaskell.shellFor { + packages = p: [ p.${package.name} ]; + withHoogle = false; + buildInputs = buildInputsCommon ++ buildInputsDevOnly; }; - runAsRoot = '' - #!${pkgs.runtimeShell} - ${pkgs.dockerTools.shadowSetup} - groupadd -r users - useradd -r -g users patron - ''; - - config = { - User = "patron"; - Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; - Cmd = null; + ## CI shell (minimal, fast): + ciShell = thisHaskell.shellFor { + packages = p: [ p.${package.name} ]; + withHoogle = false; + buildInputs = buildInputsCommon; }; - }; - in - { - ## Project packages output: - packages = { - "${package.name}" = thisPackage; - docker = thisDocker; - default = self.packages.${system}.${package.name}; - }; - ## Project development shell output: - devShells = { - default = thisShell; + thisPackage = pkgs.haskell.lib.justStaticExecutables ( + thisHaskell.${package.name}.overrideAttrs (oldAttrs: { + nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ + pkgs.git + pkgs.installShellFiles + pkgs.makeWrapper + ]; + + postFixup = (oldAttrs.postFixup or "") + '' + ## Create output directories: + mkdir -p $out/{bin} + + ## Wrap program to add PATHs to dependencies: + wrapProgram $out/bin/${package.name} --prefix PATH : ${ + pkgs.lib.makeBinPath [ + pkgs.bashInteractive # Added for bash-based CLI option completions + ] + } + + ## Install completion scripts: + installShellCompletion --bash --name ${package.name}.bash <($out/bin/${package.name} --bash-completion-script "$out/bin/${package.name}") + installShellCompletion --fish --name ${package.name}.fish <($out/bin/${package.name} --fish-completion-script "$out/bin/${package.name}") + installShellCompletion --zsh --name _${package.name} <($out/bin/${package.name} --zsh-completion-script "$out/bin/${package.name}") + ''; + }) + ); + + thisDocker = pkgs.dockerTools.buildImage { + name = "${package.name}"; + tag = "v${package.version}"; + created = "now"; + + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ pkgs.cacert ]; + pathsToLink = [ "/etc" ]; + }; + + runAsRoot = '' + #!${pkgs.runtimeShell} + ${pkgs.dockerTools.shadowSetup} + groupadd -r users + useradd -r -g users patron + ''; + + config = { + User = "patron"; + Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; + Cmd = null; + }; + }; + in + { + ## Project packages output: + packages = { + "${package.name}" = thisPackage; + docker = thisDocker; + default = thisPackage; + }; + + ## Project development shells: + devShells = { + default = devShell; + ci = ciShell; + }; }; - }); + }; } diff --git a/nix/cabal-verify/default.nix b/nix/cabal-verify/default.nix new file mode 100644 index 0000000..f385fba --- /dev/null +++ b/nix/cabal-verify/default.nix @@ -0,0 +1,26 @@ +{ + lib, + writeShellApplication, + bash, + coreutils, + moreutils, + yq-go, +}: + +writeShellApplication { + name = "cabal-verify"; + + text = builtins.readFile ./script.sh; + + runtimeInputs = [ + bash + coreutils + moreutils + yq-go + ]; + + meta = with lib; { + description = "Run project verification checks (format, lint, build, test, docs, etc...)"; + platforms = platforms.all; + }; +} diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh new file mode 100644 index 0000000..3e5cdf3 --- /dev/null +++ b/nix/cabal-verify/script.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +############### +## VARIABLES ## +############### + +_clean=false +_cabal="$(command -v cabal)" + +##################### +## TERMINAL COLORS ## +##################### + +if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then + BOLD="$(tput bold)" + RESET="$(tput sgr0)" + RED="$(tput setaf 1)" + GREEN="$(tput setaf 2)" + BLUE="$(tput setaf 4)" +else + BOLD="" + RESET="" + RED="" + GREEN="" + BLUE="" +fi + +####################### +## UTILITY FUNCTIONS ## +####################### + +_usage() { + echo "Usage: ${0} [OPTIONS]" + echo "" + echo " Runs all checks and tests for the project." + echo "" + echo "Options:" + echo " -c, --clean Clean the project before running tests." + echo " -h, --help Show this help message and exit." +} + +_suc() { + printf "%s%s✅ %s%s\n" "${BOLD}" "${GREEN}" "${1}" "${RESET}" +} + +_err() { + printf "%s%s❌ %s%s\n" "${BOLD}" "${RED}" "${1}" "${RESET}" +} + +_get_now_ms() { + date +%s%3N +} + +_diff_ms_to_s() { + local _ms="${1}" + printf '%d.%03d' "$((_ms / 1000))" "$((_ms % 1000))" +} + +_run_check() { + local _title="${1}" + shift + + printf "%s%s🔵 Running %s%s " "${BOLD}" "${BLUE}" "${_title}" "${RESET}" + + local _ms_since + local _ms_until + local _ms_total + local _captured + + _ms_since="$(_get_now_ms)" + + if _captured="$(chronic -- "${@}" 2>&1)"; then + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _suc "$(_diff_ms_to_s "${_ms_total}")s" + else + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _err "$(_diff_ms_to_s "${_ms_total}")s" + >&2 printf "%s\n" "${_captured}" + exit 1 + fi +} + +#################################### +## COMMAND-LINE ARGUMENTS PARSING ## +#################################### + +# cabal external command support +if [[ -n "${CABAL:-}" ]]; then + _cabal="${CABAL}" + shift +fi + +while [[ $# -gt 0 ]]; do + case "${1}" in + --) + shift + break + ;; + -c | --clean) + _clean=true + shift + ;; + -h | --help) + _usage + exit 0 + ;; + -*) + _usage + echo "" + >&2 _err "Invalid option: ${1}" + exit 1 + ;; + *) + break + ;; + esac +done + +############### +## PROCEDURE ## +############### + +_script_start_ms="$(_get_now_ms)" + +${_clean} && _run_check "clean" "${_cabal}" clean && _run_check "v1-clean" "${_cabal}" v1-clean + +_run_check "hpack (v$(hpack --numeric-version))" \ + hpack + +_run_check "nixfmt (v$(nixfmt --numeric-version))" \ + find . -type f -iname "*.nix" -exec nixfmt --check {} + + +_run_check "statix (v$(statix --version | cut -f2 -d" "))" \ + statix check + +_run_check "shfmt (v$(shfmt --version))" \ + find . -type f -iname "*.sh" -exec shfmt --diff {} + + +_run_check "shellcheck (v$(shellcheck --version | grep "^version" | head -n1 | cut -f2 -d" "))" \ + find . -type f -iname "*.sh" -exec shellcheck {} + + +_run_check "prettier (v$(prettier --version))" \ + prettier --check . + +_run_check "taplo lint (v$(taplo --version | cut -f2 -d" "))" \ + taplo lint + +_run_check "taplo format (v$(taplo --version | cut -f2 -d" "))" \ + taplo format --check + +_run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -f2 -d" "))" \ + fourmolu --quiet --mode check app/ src/ test/ + +_run_check "hlint (v$(hlint --numeric-version))" \ + hlint app/ src/ test/ + +_run_check "cabal build (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" build -O0 + +_run_check "cabal run (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" run "$(yq ".executables | keys | .[0]" package.yaml)" -O0 -- --version + +_run_check "cabal test (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" v1-test --ghc-options="-O0" + +_run_check "weeder (v$(weeder --version | head -n1 | cut -f3 -d" "))" \ + weeder + +_run_check "stan ($(stan --version | head -n1 | cut -f2 -d" "))" \ + stan --hiedir ./dist-newstyle + +_run_check "cabal haddock (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" haddock -O0 \ + --haddock-quickjump \ + --haddock-hyperlink-source \ + --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" + +_suc "All checks passed in $(_diff_ms_to_s $(($(_get_now_ms) - _script_start_ms)))s." diff --git a/nix/dev-test-build.sh b/nix/dev-test-build.sh deleted file mode 100644 index 7f1604e..0000000 --- a/nix/dev-test-build.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env bash - -## Purpose: This script is used to run all the necessary checks and -## tests for the project. - -## Fail on any error: -set -e - -## Declare default styles: -_sty_bold="" -_sty_underline="" -_sty_standout="" -_sty_normal="" -_sty_black="" -_sty_red="" -_sty_green="" -_sty_yellow="" -_sty_blue="" -_sty_magenta="" -_sty_cyan="" -_sty_white="" - -## Set styles if we are on terminal: -if test -t 1; then - ## Check if the terminal supports colors: - ncolors=$(tput colors) - - ## Defines styles: - if test -n "$ncolors" && test "${ncolors}" -ge 8; then - _sty_bold="$(tput bold)" - _sty_underline="$(tput smul)" - _sty_standout="$(tput smso)" - _sty_normal="$(tput sgr0)" - _sty_black="$(tput setaf 0)" - _sty_red="$(tput setaf 1)" - _sty_green="$(tput setaf 2)" - _sty_yellow="$(tput setaf 3)" - _sty_blue="$(tput setaf 4)" - _sty_magenta="$(tput setaf 5)" - _sty_cyan="$(tput setaf 6)" - _sty_white="$(tput setaf 7)" - fi -fi - -_clean="" - -while getopts ":c" opt; do - case ${opt} in - c) - _clean="true" - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done - -_get_now() { - t=${EPOCHREALTIME} # remove the decimal separator (s → µs) - t=${t%???} # remove the last three digits (µs → ms) - echo "${t}" -} - -_get_diff() { - printf "scale=3; %s - %s\n" "${2}" "${1}" | bc -} - -_print_header() { - printf "${_sty_bold}${_sty_blue}🔵 Running %s${_sty_normal}" "${1}" -} - -_print_success() { - _start="${1}" - _until="${2}" - _elapsed=$(_get_diff "${_start}" "${_until}") - printf "${_sty_bold}${_sty_green} ✅ %ss${_sty_normal}\n" "${_elapsed}" -} - -_clean() { - _print_header "clean" - _start=$(_get_now) - chronic -- cabal clean && chronic -- cabal v1-clean - _print_success "${_start}" "$(_get_now)" -} - -_hpack() { - _print_header "hpack (v$(hpack --numeric-version))" - _start=$(_get_now) - chronic -- hpack - _print_success "${_start}" "$(_get_now)" -} - -_fourmolu() { - _print_header "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- fourmolu --quiet --mode check app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_prettier() { - _print_header "prettier (v$(prettier --version))" - _start=$(_get_now) - chronic -- prettier --check . - _print_success "${_start}" "$(_get_now)" -} - -_nixpkgs_fmt() { - _print_header "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- find . -iname "*.nix" -not -path "*/nix/sources.nix" -exec nixpkgs-fmt --check {} \; - _print_success "${_start}" "$(_get_now)" -} - -_hlint() { - _print_header "hlint (v$(hlint --numeric-version))" - _start=$(_get_now) - chronic -- hlint app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_cabal_build() { - _print_header "cabal build (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal build -O0 - _print_success "${_start}" "$(_get_now)" -} - -_cabal_run() { - _print_header "cabal run (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal run -O0 clompse -- --version - _print_success "${_start}" "$(_get_now)" -} - -_cabal_test() { - _print_header "cabal test (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal v1-test - _print_success "${_start}" "$(_get_now)" -} - -_weeder() { - _print_header "weeder (v$(weeder --version | head -n1 | cut -d' ' -f3))" - _start=$(_get_now) - chronic -- weeder - _print_success "${_start}" "$(_get_now)" -} - -_cabal_haddock() { - _print_header "cabal haddock (v$(cabal --numeric-version))" - _start=$(_get_now) - chronic -- cabal haddock -O0 \ - --haddock-quickjump \ - --haddock-hyperlink-source \ - --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" - _print_success "${_start}" "$(_get_now)" -} - -_scr_start=$(_get_now) -if [ -n "${_clean}" ]; then - _clean -fi -_hpack -_fourmolu -_prettier -_nixpkgs_fmt -_hlint -_cabal_build -_cabal_run -_cabal_test -_weeder -_cabal_haddock -printf "Finished all in %ss\n" "$(_get_diff "${_scr_start}" "$(_get_now)")" diff --git a/nix/flake-modules/read-yaml/default.nix b/nix/flake-modules/read-yaml/default.nix new file mode 100644 index 0000000..689b80d --- /dev/null +++ b/nix/flake-modules/read-yaml/default.nix @@ -0,0 +1,9 @@ +_: { + perSystem = + { pkgs, ... }: + { + _module.args = { + readYAML = pkgs.callPackage ./function.nix { }; + }; + }; +} diff --git a/nix/read-yaml.nix b/nix/flake-modules/read-yaml/function.nix similarity index 76% rename from nix/read-yaml.nix rename to nix/flake-modules/read-yaml/function.nix index d89721a..472d172 100644 --- a/nix/read-yaml.nix +++ b/nix/flake-modules/read-yaml/function.nix @@ -18,10 +18,8 @@ path: let - jsonOutputDrv = - runCommand - "from-yaml" - { nativeBuildInputs = [ remarshal ]; } - "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; + jsonOutputDrv = runCommand "from-yaml" { + nativeBuildInputs = [ remarshal ]; + } "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; in builtins.fromJSON (builtins.readFile jsonOutputDrv) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..2926e56 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,46 @@ +## This resolver should correspond to our nixpkgs version (at least baseline): +resolver: lts-24.25 + +## Our local packages: +packages: + - . + +## Use the GHC provided by our Docker build image; never download another one: +system-ghc: true +install-ghc: false + +## Allow running as a different user inside Docker containers: +allow-different-user: true + +## Set the output path for our compiled binaries (stack install): +local-bin-path: /tmp + +## Configure Cabal to build static executables (no shared/dynamic): +configure-options: + "$locals": + - --disable-shared + - --disable-executable-dynamic + - --enable-executable-static + +## Make smaller libs without any profiling: +build: + library-profiling: false + library-stripping: true + +## Extra dependencies pinned to specific commits: +extra-deps: + - github: brendanhay/amazonka + commit: a7d699be1076e2aad05a1930ca3937ffea954ad8 + subdirs: + - lib/amazonka + - lib/amazonka-core + - lib/services/amazonka-ec2 + - lib/services/amazonka-lightsail + - lib/services/amazonka-route53 + - lib/services/amazonka-s3 + - lib/services/amazonka-sso + - lib/services/amazonka-sts + - github: stevenfontanella/microlens + commit: a8d75ac12735e5d9962b51822dbcd0f2103267f3 + subdirs: + - microlens-pro